From 26caaf0404057f120be417687637a17a8da416dd Mon Sep 17 00:00:00 2001 From: Igor Benav Date: Wed, 2 Jul 2025 01:49:05 -0300 Subject: [PATCH] add CRUDAdmin --- README.md | 159 +++++- docs/user-guide/admin-panel/adding-models.md | 480 ++++++++++++++++++ docs/user-guide/admin-panel/configuration.md | 378 ++++++++++++++ docs/user-guide/admin-panel/index.md | 295 +++++++++++ .../user-guide/admin-panel/user-management.md | 213 ++++++++ docs/user-guide/index.md | 8 + mkdocs.yml | 5 + pyproject.toml | 1 + src/app/admin/__init__.py | 0 src/app/admin/initialize.py | 54 ++ src/app/admin/views.py | 60 +++ src/app/core/config.py | 22 + src/app/core/setup.py | 5 +- src/app/main.py | 33 +- uv.lock | 95 +++- 15 files changed, 1789 insertions(+), 19 deletions(-) create mode 100644 docs/user-guide/admin-panel/adding-models.md create mode 100644 docs/user-guide/admin-panel/configuration.md create mode 100644 docs/user-guide/admin-panel/index.md create mode 100644 docs/user-guide/admin-panel/user-management.md create mode 100644 src/app/admin/__init__.py create mode 100644 src/app/admin/initialize.py create mode 100644 src/app/admin/views.py diff --git a/README.md b/README.md index ee9fea0..0fcf7a3 100644 --- a/README.md +++ b/README.md @@ -74,10 +74,11 @@ This README provides a quick reference for LLMs and developers, but the full doc - 🏬 Easy redis caching - 👜 Easy client-side caching - 🚦 ARQ integration for task queue -- ⚙️ Efficient and robust queries with fastcrud -- ⎘ Out of the box offset and cursor pagination support with fastcrud +- ⚙️ Efficient and robust queries with fastcrud +- ⎘ Out of the box offset and cursor pagination support with fastcrud - 🛑 Rate Limiter dependency - 👮 FastAPI docs behind authentication and hidden based on the environment +- 🔧 Modern and light admin interface powered by [CRUDAdmin](https://github.com/benavlabs/crudadmin) - 🚚 Easy running with docker compose - ⚖️ NGINX Reverse Proxy and Load Balancing @@ -114,9 +115,10 @@ This README provides a quick reference for LLMs and developers, but the full doc 1. [ARQ Job Queues](#510-arq-job-queues) 1. [Rate Limiting](#511-rate-limiting) 1. [JWT Authentication](#512-jwt-authentication) - 1. [Running](#513-running) - 1. [Create Application](#514-create-application) - 2. [Opting Out of Services](#515-opting-out-of-services) + 1. [Admin Panel](#513-admin-panel) + 1. [Running](#514-running) + 1. [Create Application](#515-create-application) + 2. [Opting Out of Services](#516-opting-out-of-services) 1. [Running in Production](#6-running-in-production) 1. [Uvicorn Workers with Gunicorn](#61-uvicorn-workers-with-gunicorn) 1. [Running With NGINX](#62-running-with-nginx) @@ -239,6 +241,37 @@ ADMIN_USERNAME="your_username" ADMIN_PASSWORD="your_password" ``` +For the CRUDAdmin panel: + +``` +# ------------- crud admin ------------- +CRUD_ADMIN_ENABLED=true # default=true, set to false to disable admin panel +CRUD_ADMIN_MOUNT_PATH="/admin" # default="/admin", path where admin panel will be mounted + +# ------------- crud admin security ------------- +CRUD_ADMIN_MAX_SESSIONS=10 # default=10, maximum concurrent sessions per user +CRUD_ADMIN_SESSION_TIMEOUT=1440 # default=1440 (24 hours), session timeout in minutes +SESSION_SECURE_COOKIES=true # default=true, use secure cookies + +# ------------- crud admin tracking ------------- +CRUD_ADMIN_TRACK_EVENTS=true # default=true, track admin events +CRUD_ADMIN_TRACK_SESSIONS=true # default=true, track admin sessions in database + +# ------------- crud admin redis (optional for production) ------------- +CRUD_ADMIN_REDIS_ENABLED=false # default=false, use Redis for session storage +CRUD_ADMIN_REDIS_HOST="localhost" # default="localhost", Redis host for admin sessions +CRUD_ADMIN_REDIS_PORT=6379 # default=6379, Redis port for admin sessions +CRUD_ADMIN_REDIS_DB=0 # default=0, Redis database for admin sessions +CRUD_ADMIN_REDIS_PASSWORD="" # optional, Redis password for admin sessions +CRUD_ADMIN_REDIS_SSL=false # default=false, use SSL for Redis connection +``` + +**Session Backend Options:** +- **Memory** (default): Development-friendly, sessions reset on restart +- **Redis** (production): High performance, scalable, persistent sessions +- **Database**: Audit-friendly with admin visibility +- **Hybrid**: Redis performance + database audit trail + For redis caching: ``` @@ -1546,7 +1579,116 @@ What you should do with the client is: This authentication setup in the provides a robust, secure, and user-friendly way to handle user sessions in your API applications. -### 5.13 Running +### 5.13 Admin Panel + +> 📖 **[See admin panel guide in our docs](https://benavlabs.github.io/FastAPI-boilerplate/user-guide/admin-panel/)** + +The boilerplate includes a powerful web-based admin interface built with [CRUDAdmin](https://github.com/benavlabs/crudadmin) that provides a comprehensive database management system. + +> **About CRUDAdmin**: CRUDAdmin is a modern admin interface generator for FastAPI applications. Learn more at: +> - **📚 Documentation**: [benavlabs.github.io/crudadmin](https://benavlabs.github.io/crudadmin/) +> - **💻 GitHub**: [github.com/benavlabs/crudadmin](https://github.com/benavlabs/crudadmin) + +#### 5.13.1 Features + +The admin panel includes: + +- **User Management**: Create, view, update users with password hashing +- **Tier Management**: Manage user tiers and permissions +- **Post Management**: Full CRUD operations for posts +- **Authentication**: Secure login system with session management +- **Security**: IP restrictions, session timeouts, and secure cookies +- **Redis Integration**: Optional Redis support for session storage +- **Event Tracking**: Track admin actions and sessions + +#### 5.13.2 Access + +Once your application is running, you can access the admin panel at: + +``` +http://localhost:8000/admin +``` + +Use the admin credentials you defined in your `.env` file: +- Username: `ADMIN_USERNAME` +- Password: `ADMIN_PASSWORD` + +#### 5.13.3 Configuration + +The admin panel is highly configurable through environment variables: + +- **Basic Settings**: Enable/disable, mount path +- **Security**: Session limits, timeouts, IP restrictions +- **Tracking**: Event and session tracking +- **Redis**: Optional Redis session storage + +See the [environment variables section](#31-environment-variables-env) for complete configuration options. + +#### 5.13.4 Customization + +**Adding New Models** + +To add new models to the admin panel, edit `src/app/admin/views.py`: + +```python +from your_app.models import YourModel +from your_app.schemas import YourCreateSchema, YourUpdateSchema + +def register_admin_views(admin: CRUDAdmin) -> None: + # ... existing models ... + + admin.add_view( + model=YourModel, + create_schema=YourCreateSchema, + update_schema=YourUpdateSchema, + allowed_actions={"view", "create", "update", "delete"} + ) +``` + +**Advanced Configuration** + +For more complex model configurations: + +```python +# Handle models with problematic fields (e.g., TSVector) +admin.add_view( + model=Article, + create_schema=ArticleCreate, + update_schema=ArticleUpdate, + select_schema=ArticleSelect, # Exclude problematic fields from read operations + allowed_actions={"view", "create", "update", "delete"} +) + +# Password field handling +admin.add_view( + model=User, + create_schema=UserCreateWithPassword, + update_schema=UserUpdateWithPassword, + password_transformer=password_transformer, # Handles password hashing + allowed_actions={"view", "create", "update"} +) + +# Read-only models +admin.add_view( + model=AuditLog, + create_schema=AuditLogSchema, + update_schema=AuditLogSchema, + allowed_actions={"view"} # Only viewing allowed +) +``` + +**Session Backend Configuration** + +For production environments, consider using Redis for better performance: + +```python +# Enable Redis sessions in your environment +CRUD_ADMIN_REDIS_ENABLED=true +CRUD_ADMIN_REDIS_HOST=localhost +CRUD_ADMIN_REDIS_PORT=6379 +``` + +### 5.14 Running If you are using docker compose, just running the following command should ensure everything is working: @@ -1566,7 +1708,7 @@ And for the worker: ```sh uv run arq src.app.core.worker.settings.WorkerSettings ``` -### 5.14 Create Application +### 5.15 Create Application If you want to stop tables from being created every time you run the api, you should disable this here: @@ -1589,7 +1731,7 @@ A few examples: - Add client-side cache middleware - Add Startup and Shutdown event handlers for cache, queue and rate limit -### 5.15 Opting Out of Services +### 5.16 Opting Out of Services To opt out of services (like `Redis`, `Queue`, `Rate Limiter`), head to the `Settings` class in `src/app/core/config`: @@ -1617,6 +1759,7 @@ class Settings( RedisQueueSettings, RedisRateLimiterSettings, DefaultRateLimitSettings, + CRUDAdminSettings, EnvironmentSettings, ): pass diff --git a/docs/user-guide/admin-panel/adding-models.md b/docs/user-guide/admin-panel/adding-models.md new file mode 100644 index 0000000..fd36852 --- /dev/null +++ b/docs/user-guide/admin-panel/adding-models.md @@ -0,0 +1,480 @@ +# Adding Models + +Learn how to extend the admin interface with your new models by following the patterns established in the FastAPI boilerplate. The boilerplate already includes User, Tier, and Post models - we'll show you how to add your own models using these working examples. + +> **CRUDAdmin Features**: This guide shows boilerplate-specific patterns. For advanced model configuration options and features, see the [CRUDAdmin documentation](https://benavlabs.github.io/crudadmin/). + +## Understanding the Existing Setup + +The boilerplate comes with three models already registered in the admin interface. Understanding how they're implemented will help you add your own models successfully. + +### Current Model Registration + +The admin interface is configured in `src/app/admin/views.py`: + +```python +def register_admin_views(admin: CRUDAdmin) -> None: + """Register all models and their schemas with the admin interface.""" + + # User model with password handling + password_transformer = PasswordTransformer( + password_field="password", + hashed_field="hashed_password", + hash_function=get_password_hash, + required_fields=["name", "username", "email"], + ) + + admin.add_view( + model=User, + create_schema=UserCreate, + update_schema=UserUpdate, + allowed_actions={"view", "create", "update"}, + password_transformer=password_transformer, + ) + + admin.add_view( + model=Tier, + create_schema=TierCreate, + update_schema=TierUpdate, + allowed_actions={"view", "create", "update", "delete"} + ) + + admin.add_view( + model=Post, + create_schema=PostCreateAdmin, # Special admin-only schema + update_schema=PostUpdate, + allowed_actions={"view", "create", "update", "delete"} + ) +``` + +Each model registration follows the same pattern: specify the SQLAlchemy model, appropriate Pydantic schemas for create/update operations, and define which actions are allowed. + +## Step-by-Step Model Addition + +Let's walk through adding a new model to your admin interface using a product catalog example. + +### Step 1: Create Your Model + +First, create your SQLAlchemy model following the boilerplate's patterns: + +```python +# src/app/models/product.py +from decimal import Decimal +from sqlalchemy.orm import Mapped, mapped_column +from sqlalchemy import String, Numeric, ForeignKey, Text, Boolean +from sqlalchemy.types import DateTime +from datetime import datetime + +from ..core.db.database import Base + +class Product(Base): + __tablename__ = "products" + + id: Mapped[int] = mapped_column(primary_key=True) + name: Mapped[str] = mapped_column(String(100), nullable=False) + description: Mapped[str | None] = mapped_column(Text, nullable=True) + price: Mapped[Decimal] = mapped_column(Numeric(10, 2), nullable=False) + is_active: Mapped[bool] = mapped_column(Boolean, default=True) + created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) + + # Foreign key relationship (similar to Post.created_by_user_id) + category_id: Mapped[int] = mapped_column(ForeignKey("categories.id")) +``` + +### Step 2: Create Pydantic Schemas + +Create schemas for the admin interface following the boilerplate's pattern: + +```python +# src/app/schemas/product.py +from decimal import Decimal +from pydantic import BaseModel, Field +from typing import Annotated + +class ProductCreate(BaseModel): + name: Annotated[str, Field(min_length=2, max_length=100)] + description: Annotated[str | None, Field(max_length=1000, default=None)] + price: Annotated[Decimal, Field(gt=0, le=999999.99)] + is_active: Annotated[bool, Field(default=True)] + category_id: Annotated[int, Field(gt=0)] + +class ProductUpdate(BaseModel): + name: Annotated[str | None, Field(min_length=2, max_length=100, default=None)] + description: Annotated[str | None, Field(max_length=1000, default=None)] + price: Annotated[Decimal | None, Field(gt=0, le=999999.99, default=None)] + is_active: Annotated[bool | None, Field(default=None)] + category_id: Annotated[int | None, Field(gt=0, default=None)] +``` + +### Step 3: Register with Admin Interface + +Add your model to `src/app/admin/views.py`: + +```python +# Add import at the top +from ..models.product import Product +from ..schemas.product import ProductCreate, ProductUpdate + +def register_admin_views(admin: CRUDAdmin) -> None: + """Register all models and their schemas with the admin interface.""" + + # ... existing model registrations ... + + # Add your new model + admin.add_view( + model=Product, + create_schema=ProductCreate, + update_schema=ProductUpdate, + allowed_actions={"view", "create", "update", "delete"} + ) +``` + +### Step 4: Create and Run Migration + +Generate the database migration for your new model: + +```bash +# Generate migration +uv run alembic revision --autogenerate -m "Add product model" + +# Apply migration +uv run alembic upgrade head +``` + +### Step 5: Test Your New Model + +Start your application and test the new model in the admin interface: + +```bash +# Start the application +uv run fastapi dev + +# Visit http://localhost:8000/admin +# Login with your admin credentials +# You should see "Products" in the admin navigation +``` + +## Learning from Existing Models + +Each model in the boilerplate demonstrates different admin interface patterns you can follow. + +### User Model - Password Handling + +The User model shows how to handle sensitive fields like passwords: + +```python +# Password transformer for secure password handling +password_transformer = PasswordTransformer( + password_field="password", # Field in the schema + hashed_field="hashed_password", # Field in the database model + hash_function=get_password_hash, # Your app's hash function + required_fields=["name", "username", "email"], # Fields required for user creation +) + +admin.add_view( + model=User, + create_schema=UserCreate, + update_schema=UserUpdate, + allowed_actions={"view", "create", "update"}, # No delete for users + password_transformer=password_transformer, +) +``` + +**When to use this pattern:** + +- Models with password fields +- Any field that needs transformation before storage +- Fields requiring special security handling + +### Tier Model - Simple CRUD + +The Tier model demonstrates straightforward CRUD operations: + +```python +admin.add_view( + model=Tier, + create_schema=TierCreate, + update_schema=TierUpdate, + allowed_actions={"view", "create", "update", "delete"} # Full CRUD +) +``` + +**When to use this pattern:** + +- Reference data (categories, types, statuses) +- Configuration models +- Simple data without complex relationships + +### Post Model - Admin-Specific Schemas + +The Post model shows how to create admin-specific schemas when the regular API schemas don't work for admin purposes: + +```python +# Special admin schema (different from regular PostCreate) +class PostCreateAdmin(BaseModel): + title: Annotated[str, Field(min_length=2, max_length=30)] + text: Annotated[str, Field(min_length=1, max_length=63206)] + created_by_user_id: int # Required in admin, but not in API + media_url: Annotated[str | None, Field(pattern=r"^(https?|ftp)://[^\s/$.?#].[^\s]*$", default=None)] + +admin.add_view( + model=Post, + create_schema=PostCreateAdmin, # Admin-specific schema + update_schema=PostUpdate, # Regular update schema works fine + allowed_actions={"view", "create", "update", "delete"} +) +``` + +**When to use this pattern:** + +- Models where admins need to set fields that users can't +- Models requiring additional validation for admin operations +- Cases where API schemas are too restrictive or too permissive for admin use + +## Advanced Model Configuration + +### Customizing Field Display + +You can control how fields appear in the admin interface by modifying your schemas: + +```python +class ProductCreateAdmin(BaseModel): + name: Annotated[str, Field( + min_length=2, + max_length=100, + description="Product name as shown to customers" + )] + description: Annotated[str | None, Field( + max_length=1000, + description="Detailed product description (supports HTML)" + )] + price: Annotated[Decimal, Field( + gt=0, + le=999999.99, + description="Price in USD (up to 2 decimal places)" + )] + category_id: Annotated[int, Field( + gt=0, + description="Product category (creates dropdown automatically)" + )] +``` + +### Restricting Actions + +Control what operations are available for each model: + +```python +# Read-only model (reports, logs, etc.) +admin.add_view( + model=AuditLog, + create_schema=None, # No creation allowed + update_schema=None, # No updates allowed + allowed_actions={"view"} # Only viewing +) + +# No deletion allowed (users, critical data) +admin.add_view( + model=User, + create_schema=UserCreate, + update_schema=UserUpdate, + allowed_actions={"view", "create", "update"} # No delete +) +``` + +### Handling Complex Fields + +Some models may have fields that don't work well in the admin interface. Use select schemas to exclude problematic fields: + +```python +from pydantic import BaseModel + +# Create a simplified view schema +class ProductAdminView(BaseModel): + id: int + name: str + price: Decimal + is_active: bool + # Exclude complex fields like large text or binary data + +admin.add_view( + model=Product, + create_schema=ProductCreate, + update_schema=ProductUpdate, + select_schema=ProductAdminView, # Controls what's shown in lists + allowed_actions={"view", "create", "update", "delete"} +) +``` + +## Common Model Patterns + +### Reference Data Models + +For categories, types, and other reference data: + +```python +# Simple reference model +class Category(Base): + __tablename__ = "categories" + id: Mapped[int] = mapped_column(primary_key=True) + name: Mapped[str] = mapped_column(String(50), unique=True) + description: Mapped[str | None] = mapped_column(Text) + +# Simple schemas +class CategoryCreate(BaseModel): + name: str = Field(..., min_length=2, max_length=50) + description: str | None = None + +# Registration +admin.add_view( + model=Category, + create_schema=CategoryCreate, + update_schema=CategoryCreate, # Same schema for create and update + allowed_actions={"view", "create", "update", "delete"} +) +``` + +### User-Generated Content + +For content models with user associations: + +```python +class BlogPost(Base): + __tablename__ = "blog_posts" + id: Mapped[int] = mapped_column(primary_key=True) + title: Mapped[str] = mapped_column(String(200)) + content: Mapped[str] = mapped_column(Text) + author_id: Mapped[int] = mapped_column(ForeignKey("users.id")) + published_at: Mapped[datetime | None] = mapped_column(DateTime) + +# Admin schema with required author +class BlogPostCreateAdmin(BaseModel): + title: str = Field(..., min_length=5, max_length=200) + content: str = Field(..., min_length=10) + author_id: int = Field(..., gt=0) # Admin must specify author + published_at: datetime | None = None + +admin.add_view( + model=BlogPost, + create_schema=BlogPostCreateAdmin, + update_schema=BlogPostUpdate, + allowed_actions={"view", "create", "update", "delete"} +) +``` + +### Configuration Models + +For application settings and configuration: + +```python +class SystemSetting(Base): + __tablename__ = "system_settings" + id: Mapped[int] = mapped_column(primary_key=True) + key: Mapped[str] = mapped_column(String(100), unique=True) + value: Mapped[str] = mapped_column(Text) + description: Mapped[str | None] = mapped_column(Text) + +# Restricted actions - settings shouldn't be deleted +admin.add_view( + model=SystemSetting, + create_schema=SystemSettingCreate, + update_schema=SystemSettingUpdate, + allowed_actions={"view", "create", "update"} # No delete +) +``` + +## Testing Your Models + +After adding models to the admin interface, test them thoroughly: + +### Manual Testing + +1. **Access**: Navigate to `/admin` and log in +2. **Create**: Try creating new records with valid and invalid data +3. **Edit**: Test updating existing records +4. **Validation**: Verify that your schema validation works correctly +5. **Relationships**: Test foreign key relationships (dropdowns should populate) + +### Development Testing + +```python +# Test your admin configuration +# src/scripts/test_admin.py +from app.admin.initialize import create_admin_interface + +def test_admin_setup(): + admin = create_admin_interface() + if admin: + print("Admin interface created successfully") + print(f"Models registered: {len(admin._views)}") + for model_name in admin._views: + print(f" - {model_name}") + else: + print("Admin interface disabled") + +if __name__ == "__main__": + test_admin_setup() +``` + +```bash +# Run the test +uv run python src/scripts/test_admin.py +``` + +## Updating Model Registration + +When you need to modify how existing models appear in the admin interface: + +### Adding Actions + +```python +# Enable deletion for a model that previously didn't allow it +admin.add_view( + model=Product, + create_schema=ProductCreate, + update_schema=ProductUpdate, + allowed_actions={"view", "create", "update", "delete"} # Added delete +) +``` + +### Changing Schemas + +```python +# Switch to admin-specific schemas +admin.add_view( + model=User, + create_schema=UserCreateAdmin, # New admin schema + update_schema=UserUpdateAdmin, # New admin schema + allowed_actions={"view", "create", "update"}, + password_transformer=password_transformer, +) +``` + +### Performance Optimization + +For models with many records, consider using select schemas to limit data: + +```python +# Only show essential fields in lists +class UserListView(BaseModel): + id: int + username: str + email: str + is_active: bool + +admin.add_view( + model=User, + create_schema=UserCreate, + update_schema=UserUpdate, + select_schema=UserListView, # Faster list loading + allowed_actions={"view", "create", "update"}, + password_transformer=password_transformer, +) +``` + +## What's Next + +With your models successfully added to the admin interface, you're ready to: + +1. **[User Management](user-management.md)** - Learn how to manage admin users and implement security best practices + +Your models are now fully integrated into the admin interface and ready for production use. The admin panel will automatically handle form generation, validation, and database operations based on your model and schema definitions. \ No newline at end of file diff --git a/docs/user-guide/admin-panel/configuration.md b/docs/user-guide/admin-panel/configuration.md new file mode 100644 index 0000000..32ca406 --- /dev/null +++ b/docs/user-guide/admin-panel/configuration.md @@ -0,0 +1,378 @@ +# Configuration + +Learn how to configure the admin panel (powered by [CRUDAdmin](https://github.com/benavlabs/crudadmin)) using the FastAPI boilerplate's built-in environment variable system. The admin panel is fully integrated with your application's configuration and requires no additional setup files or complex initialization. + +> **About CRUDAdmin**: For complete configuration options and advanced features, see the [CRUDAdmin documentation](https://benavlabs.github.io/crudadmin/). + +## Environment-Based Configuration + +The FastAPI boilerplate handles all admin panel configuration through environment variables defined in your `.env` file. This approach provides consistent configuration across development, staging, and production environments. + +```bash +# Basic admin panel configuration in .env +CRUD_ADMIN_ENABLED=true +ADMIN_USERNAME="admin" +ADMIN_PASSWORD="SecurePassword123!" +CRUD_ADMIN_MOUNT_PATH="/admin" +``` + +The configuration system automatically: + +- Validates all environment variables at startup +- Provides sensible defaults for optional settings +- Adapts security settings based on your environment (local/staging/production) +- Integrates with your application's existing security and database systems + +## Core Configuration Settings + +### Enable/Disable Admin Panel + +Control whether the admin panel is available: + +```bash +# Enable admin panel (default: true) +CRUD_ADMIN_ENABLED=true + +# Disable admin panel completely +CRUD_ADMIN_ENABLED=false +``` + +When disabled, the admin interface is not mounted and consumes no resources. + +### Admin Access Credentials + +Configure the initial admin user that's created automatically: + +```bash +# Required: Admin user credentials +ADMIN_USERNAME="your-admin-username" # Admin login username +ADMIN_PASSWORD="YourSecurePassword123!" # Admin login password + +# Optional: Additional admin user details (uses existing settings) +ADMIN_NAME="Administrator" # Display name (from FirstUserSettings) +ADMIN_EMAIL="admin@yourcompany.com" # Admin email (from FirstUserSettings) +``` + +**How this works:** + +- The admin user is created automatically when the application starts +- Only created if no admin users exist (safe for restarts) +- Uses your application's existing password hashing system +- Credentials are validated according to CRUDAdmin requirements + +### Interface Configuration + +Customize where and how the admin panel appears: + +```bash +# Admin panel URL path (default: "/admin") +CRUD_ADMIN_MOUNT_PATH="/admin" # Access at http://localhost:8000/admin +CRUD_ADMIN_MOUNT_PATH="/management" # Access at http://localhost:8000/management +CRUD_ADMIN_MOUNT_PATH="/internal" # Access at http://localhost:8000/internal +``` + +The admin panel is mounted as a sub-application at your specified path. + +## Session Management Configuration + +Control how admin users stay logged in and how sessions are managed. + +### Basic Session Settings + +```bash +# Session limits and timeouts +CRUD_ADMIN_MAX_SESSIONS=10 # Max concurrent sessions per user +CRUD_ADMIN_SESSION_TIMEOUT=1440 # Session timeout in minutes (24 hours) + +# Cookie security +SESSION_SECURE_COOKIES=true # Require HTTPS for cookies (production) +``` + +**Session behavior:** + +- Each admin login creates a new session +- Sessions expire after the timeout period of inactivity +- When max sessions are exceeded, oldest sessions are removed +- Session cookies are HTTP-only and secure (when HTTPS is enabled) + +### Memory Sessions (Development) + +For local development, sessions are stored in memory by default: + +```bash +# Development configuration +ENVIRONMENT="local" # Enables memory sessions +CRUD_ADMIN_REDIS_ENABLED=false # Explicitly disable Redis (default) +``` + +**Memory session characteristics:** + +- Fast performance with no external dependencies +- Sessions lost when application restarts +- Suitable for single-developer environments +- Not suitable for load-balanced deployments + +### Redis Sessions (Production) + +For production deployments, enable Redis session storage: + +```bash +# Enable Redis sessions +CRUD_ADMIN_REDIS_ENABLED=true + +# Redis connection settings +CRUD_ADMIN_REDIS_HOST="localhost" # Redis server hostname +CRUD_ADMIN_REDIS_PORT=6379 # Redis server port +CRUD_ADMIN_REDIS_DB=0 # Redis database number +CRUD_ADMIN_REDIS_PASSWORD="secure-pass" # Redis authentication +CRUD_ADMIN_REDIS_SSL=false # Enable SSL/TLS connection +``` + +**Redis session benefits:** + +- Sessions persist across application restarts +- Supports multiple application instances (load balancing) +- Configurable expiration and cleanup +- Production-ready scalability + +**Redis URL construction:** + +The boilerplate automatically constructs the Redis URL from your environment variables: + +```python +# Automatic URL generation in src/app/admin/initialize.py +redis_url = f"redis{'s' if settings.CRUD_ADMIN_REDIS_SSL else ''}://" +if settings.CRUD_ADMIN_REDIS_PASSWORD: + redis_url += f":{settings.CRUD_ADMIN_REDIS_PASSWORD}@" +redis_url += f"{settings.CRUD_ADMIN_REDIS_HOST}:{settings.CRUD_ADMIN_REDIS_PORT}/{settings.CRUD_ADMIN_REDIS_DB}" +``` + +## Security Configuration + +The admin panel automatically adapts its security settings based on your deployment environment. + +### Environment-Based Security + +```bash +# Environment setting affects security behavior +ENVIRONMENT="local" # Development mode +ENVIRONMENT="staging" # Staging mode +ENVIRONMENT="production" # Production mode with enhanced security +``` + +**Security changes by environment:** + +| Setting | Local | Staging | Production | +|---------|-------|---------|------------| +| **HTTPS Enforcement** | Disabled | Optional | Enabled | +| **Secure Cookies** | Optional | Recommended | Required | +| **Session Tracking** | Optional | Recommended | Enabled | +| **Event Logging** | Optional | Recommended | Enabled | + +### Audit and Tracking + +Enable comprehensive logging for compliance and security monitoring: + +```bash +# Event and session tracking +CRUD_ADMIN_TRACK_EVENTS=true # Log all admin actions +CRUD_ADMIN_TRACK_SESSIONS=true # Track session lifecycle + +# Available in admin interface +# - View all admin actions with timestamps +# - Monitor active sessions +# - Track user activity patterns +``` + +### Access Restrictions + +The boilerplate supports IP and network-based access restrictions (configured in code): + +```python +# In src/app/admin/initialize.py - customize as needed +admin = CRUDAdmin( + # ... other settings ... + allowed_ips=settings.CRUD_ADMIN_ALLOWED_IPS_LIST, # Specific IP addresses + allowed_networks=settings.CRUD_ADMIN_ALLOWED_NETWORKS_LIST, # CIDR network ranges +) +``` + +To implement IP restrictions, extend the `CRUDAdminSettings` class in `src/app/core/config.py`. + +## Integration with Application Settings + +The admin panel leverages your existing application configuration for seamless integration. + +### Shared Security Settings + +```bash +# Uses your application's main secret key +SECRET_KEY="your-application-secret-key" # Shared with admin panel + +# Inherits database settings +POSTGRES_USER="dbuser" # Admin uses same database +POSTGRES_PASSWORD="dbpass" +POSTGRES_SERVER="localhost" +POSTGRES_DB="yourapp" +``` + +### Automatic Configuration Loading + +The admin panel automatically inherits settings from your application: + +```python +# In src/app/admin/initialize.py +admin = CRUDAdmin( + session=async_get_db, # Your app's database session + SECRET_KEY=settings.SECRET_KEY.get_secret_value(), # Your app's secret key + enforce_https=settings.ENVIRONMENT == EnvironmentOption.PRODUCTION, + # ... other settings from your app configuration +) +``` + +## Deployment Examples + +### Development Environment + +Perfect for local development with minimal setup: + +```bash +# .env.development +ENVIRONMENT="local" +CRUD_ADMIN_ENABLED=true +ADMIN_USERNAME="dev-admin" +ADMIN_PASSWORD="dev123" +CRUD_ADMIN_MOUNT_PATH="/admin" + +# Memory sessions - no external dependencies +CRUD_ADMIN_REDIS_ENABLED=false + +# Optional tracking for testing +CRUD_ADMIN_TRACK_EVENTS=false +CRUD_ADMIN_TRACK_SESSIONS=false +``` + +### Staging Environment + +Staging environment with Redis but relaxed security: + +```bash +# .env.staging +ENVIRONMENT="staging" +CRUD_ADMIN_ENABLED=true +ADMIN_USERNAME="staging-admin" +ADMIN_PASSWORD="StagingPassword123!" + +# Redis sessions for testing production behavior +CRUD_ADMIN_REDIS_ENABLED=true +CRUD_ADMIN_REDIS_HOST="staging-redis.example.com" +CRUD_ADMIN_REDIS_PASSWORD="staging-redis-pass" + +# Enable tracking for testing +CRUD_ADMIN_TRACK_EVENTS=true +CRUD_ADMIN_TRACK_SESSIONS=true +SESSION_SECURE_COOKIES=true +``` + +### Production Environment + +Production-ready configuration with full security: + +```bash +# .env.production +ENVIRONMENT="production" +CRUD_ADMIN_ENABLED=true +ADMIN_USERNAME="prod-admin" +ADMIN_PASSWORD="VerySecureProductionPassword123!" + +# Redis sessions for scalability +CRUD_ADMIN_REDIS_ENABLED=true +CRUD_ADMIN_REDIS_HOST="redis.internal.company.com" +CRUD_ADMIN_REDIS_PORT=6379 +CRUD_ADMIN_REDIS_PASSWORD="ultra-secure-redis-password" +CRUD_ADMIN_REDIS_SSL=true + +# Full security and tracking +SESSION_SECURE_COOKIES=true +CRUD_ADMIN_TRACK_EVENTS=true +CRUD_ADMIN_TRACK_SESSIONS=true +CRUD_ADMIN_MAX_SESSIONS=5 +CRUD_ADMIN_SESSION_TIMEOUT=480 # 8 hours for security +``` + +### Docker Deployment + +Configure for containerized deployments: + +```yaml +# docker-compose.yml +version: '3.8' +services: + web: + build: . + environment: + - ENVIRONMENT=production + - ADMIN_USERNAME=${ADMIN_USERNAME} + - ADMIN_PASSWORD=${ADMIN_PASSWORD} + + # Redis connection + - CRUD_ADMIN_REDIS_ENABLED=true + - CRUD_ADMIN_REDIS_HOST=redis + - CRUD_ADMIN_REDIS_PORT=6379 + - CRUD_ADMIN_REDIS_PASSWORD=${REDIS_PASSWORD} + + depends_on: + - redis + - postgres + + redis: + image: redis:7-alpine + command: redis-server --requirepass ${REDIS_PASSWORD} + volumes: + - redis_data:/data +``` + +```bash +# .env file for Docker +ADMIN_USERNAME="docker-admin" +ADMIN_PASSWORD="DockerSecurePassword123!" +REDIS_PASSWORD="docker-redis-password" +``` + +## Configuration Validation + +The boilerplate automatically validates your configuration at startup and provides helpful error messages. + +### Common Configuration Issues + +**Missing Required Variables:** +```bash +# Error: Admin credentials not provided +# Solution: Add to .env +ADMIN_USERNAME="your-admin" +ADMIN_PASSWORD="your-password" +``` + +**Invalid Redis Configuration:** +```bash +# Error: Redis connection failed +# Check Redis server and credentials +CRUD_ADMIN_REDIS_HOST="correct-redis-host" +CRUD_ADMIN_REDIS_PASSWORD="correct-password" +``` + +**Security Warnings:** +```bash +# Warning: Weak admin password +# Use stronger password with mixed case, numbers, symbols +ADMIN_PASSWORD="StrongerPassword123!" +``` + +## What's Next + +With your admin panel configured, you're ready to: + +1. **[Adding Models](adding-models.md)** - Register your application models with the admin interface +2. **[User Management](user-management.md)** - Manage admin users and implement security best practices + +The configuration system provides flexibility for any deployment scenario while maintaining consistency across environments. \ No newline at end of file diff --git a/docs/user-guide/admin-panel/index.md b/docs/user-guide/admin-panel/index.md new file mode 100644 index 0000000..39a6442 --- /dev/null +++ b/docs/user-guide/admin-panel/index.md @@ -0,0 +1,295 @@ +# Admin Panel + +The FastAPI boilerplate comes with a pre-configured web-based admin interface powered by [CRUDAdmin](https://github.com/benavlabs/crudadmin) that provides instant database management capabilities. Learn how to access, configure, and customize the admin panel for your development and production needs. + +> **Powered by CRUDAdmin**: This admin panel is built with [CRUDAdmin](https://github.com/benavlabs/crudadmin), a modern admin interface generator for FastAPI applications. +> +> - **📚 CRUDAdmin Documentation**: [benavlabs.github.io/crudadmin](https://benavlabs.github.io/crudadmin/) +> - **💻 CRUDAdmin GitHub**: [github.com/benavlabs/crudadmin](https://github.com/benavlabs/crudadmin) + +## What You'll Learn + +- **[Configuration](configuration.md)** - Environment variables and deployment settings +- **[Adding Models](adding-models.md)** - Register your new models with the admin interface +- **[User Management](user-management.md)** - Manage admin users and security + +## Admin Panel Overview + +Your FastAPI boilerplate includes a fully configured admin interface that's ready to use out of the box. The admin panel automatically provides web-based management for your database models without requiring any additional setup. + +**What's Already Configured:** + +- Complete admin interface mounted at `/admin` +- User, Tier, and Post models already registered +- Automatic form generation and validation +- Session management with configurable backends +- Security features and access controls + +**Accessing the Admin Panel:** + +1. Start your application: `uv run fastapi dev` +2. Navigate to: `http://localhost:8000/admin` +3. Login with default credentials (configured via environment variables) + +## Pre-Registered Models + +The boilerplate comes with three models already set up in the admin interface: + +### User Management +```python +# Already registered in your admin +admin.add_view( + model=User, + create_schema=UserCreate, + update_schema=UserUpdate, + allowed_actions={"view", "create", "update"}, + password_transformer=password_transformer, # Automatic password hashing +) +``` + +**Features:** + +- Create and manage application users +- Automatic password hashing with bcrypt +- User profile management (name, username, email) +- Tier assignment for subscription management + +### Tier Management +```python +# Subscription tiers for your application +admin.add_view( + model=Tier, + create_schema=TierCreate, + update_schema=TierUpdate, + allowed_actions={"view", "create", "update", "delete"} +) +``` + +**Features:** + +- Manage subscription tiers and pricing +- Configure rate limits per tier +- Full CRUD operations available + +### Content Management +```python +# Post/content management +admin.add_view( + model=Post, + create_schema=PostCreateAdmin, # Special admin schema + update_schema=PostUpdate, + allowed_actions={"view", "create", "update", "delete"} +) +``` + +**Features:** + +- Manage user-generated content +- Handle media URLs and content validation +- Associate posts with users + +## Quick Start + +### 1. Set Up Admin Credentials + +Configure your admin login in your `.env` file: + +```bash +# Admin Panel Access +ADMIN_USERNAME="your-admin-username" +ADMIN_PASSWORD="YourSecurePassword123!" + +# Basic Configuration +CRUD_ADMIN_ENABLED=true +CRUD_ADMIN_MOUNT_PATH="/admin" +``` + +### 2. Start the Application + +```bash +# Development +uv run fastapi dev + +# The admin panel will be available at: +# http://localhost:8000/admin +``` + +### 3. Login and Explore + +1. **Access**: Navigate to `/admin` in your browser +2. **Login**: Use the credentials from your environment variables +3. **Explore**: Browse the pre-configured models (Users, Tiers, Posts) + +## Environment Configuration + +The admin panel is configured entirely through environment variables, making it easy to adapt for different deployment environments. + +### Basic Settings + +```bash +# Enable/disable admin panel +CRUD_ADMIN_ENABLED=true # Set to false to disable completely + +# Admin interface path +CRUD_ADMIN_MOUNT_PATH="/admin" # Change the URL path + +# Admin user credentials (created automatically) +ADMIN_USERNAME="admin" # Your admin username +ADMIN_PASSWORD="SecurePassword123!" # Your admin password +``` + +### Session Management + +```bash +# Session configuration +CRUD_ADMIN_MAX_SESSIONS=10 # Max concurrent sessions per user +CRUD_ADMIN_SESSION_TIMEOUT=1440 # Session timeout (24 hours) +SESSION_SECURE_COOKIES=true # HTTPS-only cookies +``` + +### Production Security + +```bash +# Security settings for production +ENVIRONMENT="production" # Enables HTTPS enforcement +CRUD_ADMIN_TRACK_EVENTS=true # Log admin actions +CRUD_ADMIN_TRACK_SESSIONS=true # Track session activity +``` + +### Redis Session Storage + +For production deployments with multiple server instances: + +```bash +# Enable Redis sessions +CRUD_ADMIN_REDIS_ENABLED=true +CRUD_ADMIN_REDIS_HOST="localhost" +CRUD_ADMIN_REDIS_PORT=6379 +CRUD_ADMIN_REDIS_DB=0 +CRUD_ADMIN_REDIS_PASSWORD="your-redis-password" +CRUD_ADMIN_REDIS_SSL=false +``` + +## How It Works + +The admin panel integrates seamlessly with your FastAPI application through several key components: + +### Automatic Initialization + +```python +# In src/app/main.py - already configured +admin = create_admin_interface() + +@asynccontextmanager +async def lifespan_with_admin(app: FastAPI): + async with default_lifespan(app): + if admin: + await admin.initialize() # Sets up admin database + yield + +# Admin is mounted automatically at your configured path +if admin: + app.mount(settings.CRUD_ADMIN_MOUNT_PATH, admin.app) +``` + +### Configuration Integration + +```python +# In src/app/admin/initialize.py - uses your existing settings +admin = CRUDAdmin( + session=async_get_db, # Your database session + SECRET_KEY=settings.SECRET_KEY, # Your app's secret key + mount_path=settings.CRUD_ADMIN_MOUNT_PATH, # Configurable path + secure_cookies=settings.SESSION_SECURE_COOKIES, + enforce_https=settings.ENVIRONMENT == EnvironmentOption.PRODUCTION, + # ... all configured via environment variables +) +``` + +### Model Registration + +```python +# In src/app/admin/views.py - pre-configured models +def register_admin_views(admin: CRUDAdmin): + # Password handling for User model + password_transformer = PasswordTransformer( + password_field="password", + hashed_field="hashed_password", + hash_function=get_password_hash, # Uses your app's password hashing + ) + + # Register your models with appropriate schemas + admin.add_view(model=User, create_schema=UserCreate, ...) + admin.add_view(model=Tier, create_schema=TierCreate, ...) + admin.add_view(model=Post, create_schema=PostCreateAdmin, ...) +``` + +## Development vs Production + +### Development Setup + +For local development, minimal configuration is needed: + +```bash +# .env for development +CRUD_ADMIN_ENABLED=true +ADMIN_USERNAME="admin" +ADMIN_PASSWORD="admin123" +ENVIRONMENT="local" + +# Uses memory sessions (fast, no external dependencies) +CRUD_ADMIN_REDIS_ENABLED=false +``` + +### Production Setup + +For production deployments, enable additional security features: + +```bash +# .env for production +CRUD_ADMIN_ENABLED=true +ADMIN_USERNAME="production-admin" +ADMIN_PASSWORD="VerySecureProductionPassword123!" +ENVIRONMENT="production" + +# Redis sessions for scalability +CRUD_ADMIN_REDIS_ENABLED=true +CRUD_ADMIN_REDIS_HOST="your-redis-host" +CRUD_ADMIN_REDIS_PASSWORD="secure-redis-password" +CRUD_ADMIN_REDIS_SSL=true + +# Enhanced security +SESSION_SECURE_COOKIES=true +CRUD_ADMIN_TRACK_EVENTS=true +CRUD_ADMIN_TRACK_SESSIONS=true +``` + +## Getting Started Guide + +### 1. **[Configuration](configuration.md)** - Environment Setup + +Learn about all available environment variables and how to configure the admin panel for different deployment scenarios. Understand session backends and security settings. + +Perfect for setting up development environments and preparing for production deployment. + +### 2. **[Adding Models](adding-models.md)** - Extend the Admin Interface + +Discover how to register your new models with the admin interface. Learn from the existing User, Tier, and Post implementations to add your own models. + +Essential when you create new database models and want them managed through the admin interface. + +### 3. **[User Management](user-management.md)** - Admin Security + +Understand how admin authentication works, how to create additional admin users, and implement security best practices for production environments. + +Critical for production deployments where multiple team members need admin access. + +## What's Next + +Ready to start using your admin panel? Follow this path: + +1. **[Configuration](configuration.md)** - Set up your environment variables and understand deployment options +2. **[Adding Models](adding-models.md)** - Add your new models to the admin interface +3. **[User Management](user-management.md)** - Implement secure admin authentication + +The admin panel is ready to use immediately with sensible defaults, and each guide shows you how to customize it for your specific needs. \ No newline at end of file diff --git a/docs/user-guide/admin-panel/user-management.md b/docs/user-guide/admin-panel/user-management.md new file mode 100644 index 0000000..53d84f9 --- /dev/null +++ b/docs/user-guide/admin-panel/user-management.md @@ -0,0 +1,213 @@ +# User Management + +Learn how to manage admin users in your FastAPI boilerplate's admin panel. The boilerplate automatically creates admin users from environment variables and provides a separate authentication system (powered by [CRUDAdmin](https://github.com/benavlabs/crudadmin)) from your application users. + +> **CRUDAdmin Authentication**: For advanced authentication features and session management, see the [CRUDAdmin documentation](https://benavlabs.github.io/crudadmin/). + +## Initial Admin Setup + +### Configure Admin Credentials + +Set your admin credentials in your `.env` file: + +```bash +# Required admin credentials +ADMIN_USERNAME="admin" +ADMIN_PASSWORD="SecurePassword123!" + +# Optional details +ADMIN_NAME="Administrator" +ADMIN_EMAIL="admin@yourcompany.com" +``` + +### Access the Admin Panel + +Start your application and access the admin panel: + +```bash +# Start application +uv run fastapi dev + +# Visit: http://localhost:8000/admin +# Login with your ADMIN_USERNAME and ADMIN_PASSWORD +``` + +The boilerplate automatically creates the initial admin user from your environment variables when the application starts. + +## Managing Admin Users + +### Creating Additional Admin Users + +Once logged in, you can create more admin users through the admin interface: + +1. Navigate to the admin users section in the admin panel +2. Click "Create" or "Add New" +3. Fill in the required fields: + - Username (must be unique) + - Password (will be hashed automatically) + - Email (optional) + +### Admin User Requirements + +- **Username**: 3-50 characters, letters/numbers/underscores/hyphens +- **Password**: Minimum 8 characters with mixed case, numbers, and symbols +- **Email**: Valid email format (optional) + +### Updating and Removing Users + +- **Update**: Find the user in the admin panel and click "Edit" +- **Remove**: Click "Delete" (ensure you have alternative admin access first) + +## Security Configuration + +### Environment-Specific Settings + +Configure different security levels for each environment: + +```bash +# Development +ADMIN_USERNAME="dev-admin" +ADMIN_PASSWORD="DevPass123!" +ENVIRONMENT="local" + +# Production +ADMIN_USERNAME="prod-admin" +ADMIN_PASSWORD="VerySecurePassword123!" +ENVIRONMENT="production" +CRUD_ADMIN_TRACK_EVENTS=true +CRUD_ADMIN_TRACK_SESSIONS=true +SESSION_SECURE_COOKIES=true +``` + +### Session Management + +Control admin sessions with these settings: + +```bash +# Session limits and timeouts +CRUD_ADMIN_MAX_SESSIONS=10 # Max concurrent sessions per user +CRUD_ADMIN_SESSION_TIMEOUT=1440 # Timeout in minutes (24 hours) +SESSION_SECURE_COOKIES=true # HTTPS-only cookies +``` + +### Enable Tracking + +Monitor admin activity by enabling event tracking: + +```bash +# Track admin actions and sessions +CRUD_ADMIN_TRACK_EVENTS=true # Log all admin actions +CRUD_ADMIN_TRACK_SESSIONS=true # Track session lifecycle +``` + +## Production Deployment + +### Secure Credential Management + +For production, use Docker secrets or Kubernetes secrets instead of plain text: + +```yaml +# docker-compose.yml +services: + web: + secrets: + - admin_username + - admin_password + environment: + - ADMIN_USERNAME_FILE=/run/secrets/admin_username + - ADMIN_PASSWORD_FILE=/run/secrets/admin_password + +secrets: + admin_username: + file: ./secrets/admin_username.txt + admin_password: + file: ./secrets/admin_password.txt +``` + +### Production Security Settings + +```bash +# Production .env +ENVIRONMENT="production" +ADMIN_USERNAME="prod-admin" +ADMIN_PASSWORD="UltraSecurePassword123!" + +# Enhanced security +CRUD_ADMIN_REDIS_ENABLED=true +CRUD_ADMIN_REDIS_HOST="redis.internal.company.com" +CRUD_ADMIN_REDIS_PASSWORD="secure-redis-password" +CRUD_ADMIN_REDIS_SSL=true + +# Monitoring +CRUD_ADMIN_TRACK_EVENTS=true +CRUD_ADMIN_TRACK_SESSIONS=true +SESSION_SECURE_COOKIES=true +CRUD_ADMIN_MAX_SESSIONS=5 +CRUD_ADMIN_SESSION_TIMEOUT=480 # 8 hours +``` + +## Application User Management + +### Admin vs Application Users + +Your boilerplate maintains two separate user systems: + +- **Admin Users**: Access the admin panel (stored by CRUDAdmin) +- **Application Users**: Use your application (stored in your User model) + +### Managing Application Users + +Through the admin panel, you can manage your application's users: + +1. Navigate to "Users" section (your application users) +2. View, create, update user profiles +3. Manage user tiers and subscriptions +4. View user-generated content (posts) + +The User model is already registered with password hashing and proper permissions. + +## Emergency Recovery + +### Lost Admin Password + +If you lose admin access, update your environment variables: + +```bash +# Update .env file +ADMIN_USERNAME="emergency-admin" +ADMIN_PASSWORD="EmergencyPassword123!" + +# Restart application +uv run fastapi dev +``` + +### Database Recovery (Advanced) + +For direct database password reset: + +```python +# Generate bcrypt hash +import bcrypt +password = "NewPassword123!" +hashed = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()) +print(hashed.decode('utf-8')) +``` + +```sql +-- Update in database +UPDATE admin_users +SET password_hash = '' +WHERE username = 'admin'; +``` + +## What's Next + +Your admin user management is now configured with: + +- Automatic admin user creation from environment variables +- Secure authentication separate from application users +- Environment-specific security settings +- Production-ready credential management +- Emergency recovery procedures + +You can now securely manage both admin users and your application users through the admin panel. diff --git a/docs/user-guide/index.md b/docs/user-guide/index.md index 2ac23fd..2770266 100644 --- a/docs/user-guide/index.md +++ b/docs/user-guide/index.md @@ -32,6 +32,14 @@ This guide covers all aspects of working with the FastAPI Boilerplate: - **[User Management](authentication/user-management.md)** - Handle user registration, login, and profiles - **[Permissions](authentication/permissions.md)** - Implement role-based access control +### Admin Panel +Powered by [CRUDAdmin](https://github.com/benavlabs/crudadmin) - a modern admin interface generator for FastAPI. + +- **[Admin Panel Overview](admin-panel/index.md)** - Web-based database management interface +- **[Configuration](admin-panel/configuration.md)** - Setup, session backends, and environment variables +- **[Adding Models](admin-panel/adding-models.md)** - Register models, schemas, and customization +- **[User Management](admin-panel/user-management.md)** - Admin users, authentication, and security + ### Performance & Caching - **[Caching Overview](caching/index.md)** - Improve performance with Redis caching - **[Redis Cache](caching/redis-cache.md)** - Server-side caching with Redis diff --git a/mkdocs.yml b/mkdocs.yml index 7f48de3..96f2574 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -77,6 +77,11 @@ nav: - JWT Tokens: user-guide/authentication/jwt-tokens.md - User Management: user-guide/authentication/user-management.md - Permissions: user-guide/authentication/permissions.md + - Admin Panel: + - user-guide/admin-panel/index.md + - Configuration: user-guide/admin-panel/configuration.md + - Adding Models: user-guide/admin-panel/adding-models.md + - User Management: user-guide/admin-panel/user-management.md - Caching: - Overview: user-guide/caching/index.md - Redis Cache: user-guide/caching/redis-cache.md diff --git a/pyproject.toml b/pyproject.toml index 179d3bb..9e08abf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,6 +28,7 @@ dependencies = [ "bcrypt>=4.1.1", "psycopg2-binary>=2.9.9", "fastcrud>=0.15.5", + "crudadmin>=0.4.2", "gunicorn>=23.0.0", "ruff>=0.11.13", "mypy>=1.16.0", diff --git a/src/app/admin/__init__.py b/src/app/admin/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/admin/initialize.py b/src/app/admin/initialize.py new file mode 100644 index 0000000..e7ed8df --- /dev/null +++ b/src/app/admin/initialize.py @@ -0,0 +1,54 @@ +from typing import Optional + +from crudadmin import CRUDAdmin + +from ..core.config import EnvironmentOption, settings +from ..core.db.database import async_get_db +from .views import register_admin_views + + +def create_admin_interface() -> Optional[CRUDAdmin]: + """Create and configure the admin interface.""" + if not settings.CRUD_ADMIN_ENABLED: + return None + + session_backend = "memory" + redis_config = None + + if settings.CRUD_ADMIN_REDIS_ENABLED: + session_backend = "redis" + redis_config = { + "host": settings.CRUD_ADMIN_REDIS_HOST, + "port": settings.CRUD_ADMIN_REDIS_PORT, + "db": settings.CRUD_ADMIN_REDIS_DB, + "password": settings.CRUD_ADMIN_REDIS_PASSWORD if settings.CRUD_ADMIN_REDIS_PASSWORD != "None" else None, + "ssl": settings.CRUD_ADMIN_REDIS_SSL, + } + + admin = CRUDAdmin( + session=async_get_db, + SECRET_KEY=settings.SECRET_KEY.get_secret_value(), + mount_path=settings.CRUD_ADMIN_MOUNT_PATH, + session_backend=session_backend, + redis_config=redis_config, + allowed_ips=settings.CRUD_ADMIN_ALLOWED_IPS_LIST if settings.CRUD_ADMIN_ALLOWED_IPS_LIST else None, + allowed_networks=settings.CRUD_ADMIN_ALLOWED_NETWORKS_LIST + if settings.CRUD_ADMIN_ALLOWED_NETWORKS_LIST + else None, + max_sessions_per_user=settings.CRUD_ADMIN_MAX_SESSIONS, + session_timeout_minutes=settings.CRUD_ADMIN_SESSION_TIMEOUT, + secure_cookies=settings.SESSION_SECURE_COOKIES, + enforce_https=settings.ENVIRONMENT == EnvironmentOption.PRODUCTION, + track_events=settings.CRUD_ADMIN_TRACK_EVENTS, + track_sessions_in_db=settings.CRUD_ADMIN_TRACK_SESSIONS, + initial_admin={ + "username": settings.ADMIN_USERNAME, + "password": settings.ADMIN_PASSWORD, + } + if settings.ADMIN_USERNAME and settings.ADMIN_PASSWORD + else None, + ) + + register_admin_views(admin) + + return admin diff --git a/src/app/admin/views.py b/src/app/admin/views.py new file mode 100644 index 0000000..1df1db2 --- /dev/null +++ b/src/app/admin/views.py @@ -0,0 +1,60 @@ +from typing import Annotated + +from crudadmin import CRUDAdmin +from crudadmin.admin_interface.model_view import PasswordTransformer +from pydantic import BaseModel, Field + +from ..core.security import get_password_hash +from ..models.post import Post +from ..models.tier import Tier +from ..models.user import User +from ..schemas.post import PostUpdate +from ..schemas.tier import TierCreate, TierUpdate +from ..schemas.user import UserCreate, UserUpdate + + +class PostCreateAdmin(BaseModel): + title: Annotated[str, Field(min_length=2, max_length=30, examples=["This is my post"])] + text: Annotated[str, Field(min_length=1, max_length=63206, examples=["This is the content of my post."])] + created_by_user_id: int + media_url: Annotated[ + str | None, + Field(pattern=r"^(https?|ftp)://[^\s/$.?#].[^\s]*$", examples=["https://www.postimageurl.com"], default=None), + ] + + +def register_admin_views(admin: CRUDAdmin) -> None: + """Register all models and their schemas with the admin interface. + + This function adds all available models to the admin interface with appropriate + schemas and permissions. + """ + + password_transformer = PasswordTransformer( + password_field="password", + hashed_field="hashed_password", + hash_function=get_password_hash, + required_fields=["name", "username", "email"], + ) + + admin.add_view( + model=User, + create_schema=UserCreate, + update_schema=UserUpdate, + allowed_actions={"view", "create", "update"}, + password_transformer=password_transformer, + ) + + admin.add_view( + model=Tier, + create_schema=TierCreate, + update_schema=TierUpdate, + allowed_actions={"view", "create", "update", "delete"}, + ) + + admin.add_view( + model=Post, + create_schema=PostCreateAdmin, + update_schema=PostUpdate, + allowed_actions={"view", "create", "update", "delete"}, + ) diff --git a/src/app/core/config.py b/src/app/core/config.py index c925ac0..fa38a0d 100644 --- a/src/app/core/config.py +++ b/src/app/core/config.py @@ -96,6 +96,27 @@ class DefaultRateLimitSettings(BaseSettings): DEFAULT_RATE_LIMIT_PERIOD: int = config("DEFAULT_RATE_LIMIT_PERIOD", default=3600) +class CRUDAdminSettings(BaseSettings): + CRUD_ADMIN_ENABLED: bool = config("CRUD_ADMIN_ENABLED", default=True) + CRUD_ADMIN_MOUNT_PATH: str = config("CRUD_ADMIN_MOUNT_PATH", default="/admin") + + CRUD_ADMIN_ALLOWED_IPS_LIST: list[str] | None = None + CRUD_ADMIN_ALLOWED_NETWORKS_LIST: list[str] | None = None + CRUD_ADMIN_MAX_SESSIONS: int = config("CRUD_ADMIN_MAX_SESSIONS", default=10) + CRUD_ADMIN_SESSION_TIMEOUT: int = config("CRUD_ADMIN_SESSION_TIMEOUT", default=1440) + SESSION_SECURE_COOKIES: bool = config("SESSION_SECURE_COOKIES", default=True) + + CRUD_ADMIN_TRACK_EVENTS: bool = config("CRUD_ADMIN_TRACK_EVENTS", default=True) + CRUD_ADMIN_TRACK_SESSIONS: bool = config("CRUD_ADMIN_TRACK_SESSIONS", default=True) + + CRUD_ADMIN_REDIS_ENABLED: bool = config("CRUD_ADMIN_REDIS_ENABLED", default=False) + CRUD_ADMIN_REDIS_HOST: str = config("CRUD_ADMIN_REDIS_HOST", default="localhost") + CRUD_ADMIN_REDIS_PORT: int = config("CRUD_ADMIN_REDIS_PORT", default=6379) + CRUD_ADMIN_REDIS_DB: int = config("CRUD_ADMIN_REDIS_DB", default=0) + CRUD_ADMIN_REDIS_PASSWORD: str | None = config("CRUD_ADMIN_REDIS_PASSWORD", default="None") + CRUD_ADMIN_REDIS_SSL: bool = config("CRUD_ADMIN_REDIS_SSL", default=False) + + class EnvironmentOption(Enum): LOCAL = "local" STAGING = "staging" @@ -117,6 +138,7 @@ class Settings( RedisQueueSettings, RedisRateLimiterSettings, DefaultRateLimitSettings, + CRUDAdminSettings, EnvironmentSettings, ): pass diff --git a/src/app/core/setup.py b/src/app/core/setup.py index ea924ce..8e6bb81 100644 --- a/src/app/core/setup.py +++ b/src/app/core/setup.py @@ -140,6 +140,7 @@ def create_application( | EnvironmentSettings ), create_tables_on_start: bool = True, + lifespan: Callable[[FastAPI], _AsyncGeneratorContextManager[Any]] | None = None, **kwargs: Any, ) -> FastAPI: """Creates and configures a FastAPI application based on the provided settings. @@ -195,7 +196,9 @@ def create_application( if isinstance(settings, EnvironmentSettings): kwargs.update({"docs_url": None, "redoc_url": None, "openapi_url": None}) - lifespan = lifespan_factory(settings, create_tables_on_start=create_tables_on_start) + # Use custom lifespan if provided, otherwise use default factory + if lifespan is None: + lifespan = lifespan_factory(settings, create_tables_on_start=create_tables_on_start) application = FastAPI(lifespan=lifespan, **kwargs) application.include_router(router) diff --git a/src/app/main.py b/src/app/main.py index c0a81e2..6e72ab2 100644 --- a/src/app/main.py +++ b/src/app/main.py @@ -1,5 +1,34 @@ +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager + +from fastapi import FastAPI + +from .admin.initialize import create_admin_interface from .api import router from .core.config import settings -from .core.setup import create_application +from .core.setup import create_application, lifespan_factory + +admin = create_admin_interface() + + +@asynccontextmanager +async def lifespan_with_admin(app: FastAPI) -> AsyncGenerator[None, None]: + """Custom lifespan that includes admin initialization.""" + # Get the default lifespan + default_lifespan = lifespan_factory(settings) + + # Run the default lifespan initialization and our admin initialization + async with default_lifespan(app): + # Initialize admin interface if it exists + if admin: + # Initialize admin database and setup + await admin.initialize() + + yield + + +app = create_application(router=router, settings=settings, lifespan=lifespan_with_admin) -app = create_application(router=router, settings=settings) +# Mount admin interface if enabled +if admin: + app.mount(settings.CRUD_ADMIN_MOUNT_PATH, admin.app) diff --git a/uv.lock b/uv.lock index acced8b..e764621 100644 --- a/uv.lock +++ b/uv.lock @@ -1,7 +1,18 @@ version = 1 -revision = 1 requires-python = ">=3.11, <4" +[[package]] +name = "aiosqlite" +version = "0.21.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/13/7d/8bca2bf9a247c2c5dfeec1d7a5f40db6518f88d314b8bca9da29670d2671/aiosqlite-0.21.0.tar.gz", hash = "sha256:131bb8056daa3bc875608c631c678cda73922a2d4ba8aec373b19f18c17e7aa3", size = 13454 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f5/10/6c25ed6de94c49f88a91fa5018cb4c0f3625f31d5be9f771ebe5cc7cd506/aiosqlite-0.21.0-py3-none-any.whl", hash = "sha256:2549cf4057f95f53dcba16f2b64e8e2791d7e1adedb13197dd8ed77bb226d7d0", size = 15792 }, +] + [[package]] name = "alembic" version = "1.16.1" @@ -222,6 +233,29 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, ] +[[package]] +name = "crudadmin" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiosqlite" }, + { name = "bcrypt" }, + { name = "fastapi" }, + { name = "fastcrud" }, + { name = "greenlet" }, + { name = "jinja2" }, + { name = "pydantic", extra = ["email"] }, + { name = "pydantic-settings" }, + { name = "python-jose" }, + { name = "python-multipart" }, + { name = "sqlalchemy" }, + { name = "user-agents" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6e/f9/2a202981d7508d327cb969af23e4236adf0988d9873d979be4af8490c028/crudadmin-0.4.2.tar.gz", hash = "sha256:6bcfaedbaddc5bbefb9960b6a0bf7d8b75d6bf0f880b625ad3f6293a085cd31a", size = 189902 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/49/8f1f51346756c0ceb11ef9309cabb29f3d333c097bae4b4cd69f7bf0beab/crudadmin-0.4.2-py3-none-any.whl", hash = "sha256:8bba024031505eb8f7454a23c4a3690144ae4a49e0366e02d320b6374b2a9c5c", size = 217454 }, +] + [[package]] name = "cryptography" version = "45.0.3" @@ -311,16 +345,16 @@ wheels = [ [[package]] name = "fastapi" -version = "0.115.2" +version = "0.115.14" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, { name = "starlette" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/22/fa/19e3c7c9b31ac291987c82e959f36f88840bea183fa3dc3bb654669f19c1/fastapi-0.115.2.tar.gz", hash = "sha256:3995739e0b09fa12f984bce8fa9ae197b35d433750d3d312422d846e283697ee", size = 299968 } +sdist = { url = "https://files.pythonhosted.org/packages/ca/53/8c38a874844a8b0fa10dd8adf3836ac154082cf88d3f22b544e9ceea0a15/fastapi-0.115.14.tar.gz", hash = "sha256:b1de15cdc1c499a4da47914db35d0e4ef8f1ce62b624e94e0e5824421df99739", size = 296263 } wheels = [ - { url = "https://files.pythonhosted.org/packages/c9/14/bbe7776356ef01f830f8085ca3ac2aea59c73727b6ffaa757abeb7d2900b/fastapi-0.115.2-py3-none-any.whl", hash = "sha256:61704c71286579cc5a598763905928f24ee98bfcc07aabe84cfefb98812bbc86", size = 94650 }, + { url = "https://files.pythonhosted.org/packages/53/50/b1222562c6d270fea83e9c9075b8e8600b8479150a18e4516a6138b980d1/fastapi-0.115.14-py3-none-any.whl", hash = "sha256:6c0c8bf9420bd58f565e585036d971872472b4f7d3f6c73b698e10cffdefb3ca", size = 95514 }, ] [[package]] @@ -332,6 +366,7 @@ dependencies = [ { name = "arq" }, { name = "asyncpg" }, { name = "bcrypt" }, + { name = "crudadmin" }, { name = "fastapi" }, { name = "fastcrud" }, { name = "greenlet" }, @@ -375,6 +410,7 @@ requires-dist = [ { name = "arq", specifier = ">=0.25.0" }, { name = "asyncpg", specifier = ">=0.29.0" }, { name = "bcrypt", specifier = ">=4.1.1" }, + { name = "crudadmin", specifier = ">=0.4.2" }, { name = "faker", marker = "extra == 'dev'", specifier = ">=26.0.0" }, { name = "fastapi", specifier = ">=0.109.1" }, { name = "fastcrud", specifier = ">=0.15.5" }, @@ -402,14 +438,13 @@ requires-dist = [ { name = "uvicorn", specifier = ">=0.27.0" }, { name = "uvloop", specifier = ">=0.19.0" }, ] -provides-extras = ["dev"] [package.metadata.requires-dev] dev = [{ name = "pytest-asyncio", specifier = ">=1.0.0" }] [[package]] name = "fastcrud" -version = "0.15.11" +version = "0.15.12" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "fastapi" }, @@ -417,9 +452,9 @@ dependencies = [ { name = "sqlalchemy" }, { name = "sqlalchemy-utils" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/69/39/85d81942836607799e54256234c8013e07f4b60323bed140bc42f36a005c/fastcrud-0.15.11.tar.gz", hash = "sha256:204079801530f7410fb289de8a38b37a457a0b4a764b8166e493c67d112f5066", size = 40907 } +sdist = { url = "https://files.pythonhosted.org/packages/3e/53/f75ee9b3661761dd9dcd27bcf5cd95ba8b0f1df67b43c4953e34deab5a26/fastcrud-0.15.12.tar.gz", hash = "sha256:8fe76abd176e8f506e4cf6193f350a291e1932a3a1c3606c2f8bc26b992c4ca4", size = 41216 } wheels = [ - { url = "https://files.pythonhosted.org/packages/25/a8/4deb6c799885aca29769d4041344b026b9a297679a65a835a5231c1eebfd/fastcrud-0.15.11-py3-none-any.whl", hash = "sha256:046c9ed4e43cc4520f8d9be73b3a7bd9f05d6dd0d497cf2ed2b3a75ac6d3d8a3", size = 55386 }, + { url = "https://files.pythonhosted.org/packages/bb/5c/3be53d780d58b99e94cc5c0f4e2a9b8573aac2713e62c9aade02176b9aec/fastcrud-0.15.12-py3-none-any.whl", hash = "sha256:8a828a2f838f437cd7fec8bde639b45b4e63d5482bd725c5e2214a9c9d2493df", size = 55747 }, ] [[package]] @@ -613,6 +648,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050 }, ] +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899 }, +] + [[package]] name = "mako" version = "1.3.10" @@ -1202,6 +1249,38 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839 }, ] +[[package]] +name = "ua-parser" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ua-parser-builtins" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/70/0e/ed98be735bc89d5040e0c60f5620d0b8c04e9e7da99ed1459e8050e90a77/ua_parser-1.0.1.tar.gz", hash = "sha256:f9d92bf19d4329019cef91707aecc23c6d65143ad7e29a233f0580fb0d15547d", size = 728106 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/37/be6dfbfa45719aa82c008fb4772cfe5c46db765a2ca4b6f524a1fdfee4d7/ua_parser-1.0.1-py3-none-any.whl", hash = "sha256:b059f2cb0935addea7e551251cbbf42e9a8872f86134163bc1a4f79e0945ffea", size = 31410 }, +] + +[[package]] +name = "ua-parser-builtins" +version = "0.18.0.post1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/d3/13adff37f15489c784cc7669c35a6c3bf94b87540229eedf52ef2a1d0175/ua_parser_builtins-0.18.0.post1-py3-none-any.whl", hash = "sha256:eb4f93504040c3a990a6b0742a2afd540d87d7f9f05fd66e94c101db1564674d", size = 86077 }, +] + +[[package]] +name = "user-agents" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ua-parser" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e3/e1/63c5bfb485a945010c8cbc7a52f85573561737648d36b30394248730a7bc/user-agents-2.2.0.tar.gz", hash = "sha256:d36d25178db65308d1458c5fa4ab39c9b2619377010130329f3955e7626ead26", size = 9525 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/1c/20bb3d7b2bad56d881e3704131ddedbb16eb787101306887dff349064662/user_agents-2.2.0-py3-none-any.whl", hash = "sha256:a98c4dc72ecbc64812c4534108806fb0a0b3a11ec3fd1eafe807cee5b0a942e7", size = 9614 }, +] + [[package]] name = "uuid" version = "1.30"