
- Changed the Google and Apple redirect URIs in the configuration to include the API version in the path. - Reorganized the inclusion of OAuth routes in the main application to ensure they are properly prefixed and accessible. These updates aim to enhance the API structure and ensure consistency in the authentication flow.
207 lines
8.2 KiB
Python
207 lines
8.2 KiB
Python
# app/config.py
|
|
import os
|
|
from pydantic_settings import BaseSettings
|
|
from dotenv import load_dotenv
|
|
import logging
|
|
import secrets
|
|
from typing import List
|
|
|
|
load_dotenv()
|
|
logger = logging.getLogger(__name__)
|
|
|
|
class Settings(BaseSettings):
|
|
DATABASE_URL: str | None = None
|
|
GEMINI_API_KEY: str | None = None
|
|
SENTRY_DSN: str | None = None # Sentry DSN for error tracking
|
|
|
|
# --- Environment Settings ---
|
|
ENVIRONMENT: str = "development" # development, staging, production
|
|
|
|
# --- JWT Settings --- (SECRET_KEY is used by FastAPI-Users)
|
|
SECRET_KEY: str # Must be set via environment variable
|
|
TOKEN_TYPE: str = "bearer" # Default token type for JWT authentication
|
|
# FastAPI-Users handles JWT algorithm internally
|
|
|
|
# --- OCR Settings ---
|
|
MAX_FILE_SIZE_MB: int = 10 # Maximum allowed file size for OCR processing
|
|
ALLOWED_IMAGE_TYPES: list[str] = ["image/jpeg", "image/png", "image/webp"] # Supported image formats
|
|
OCR_ITEM_EXTRACTION_PROMPT: str = """
|
|
Extract the shopping list items from this image.
|
|
List each distinct item on a new line.
|
|
Ignore prices, quantities, store names, discounts, taxes, totals, and other non-item text.
|
|
Focus only on the names of the products or items to be purchased.
|
|
Add 2 underscores before and after the item name, if it is struck through.
|
|
If the image does not appear to be a shopping list or receipt, state that clearly.
|
|
Example output for a grocery list:
|
|
Milk
|
|
Eggs
|
|
Bread
|
|
__Apples__
|
|
Organic Bananas
|
|
"""
|
|
# --- OCR Error Messages ---
|
|
OCR_SERVICE_UNAVAILABLE: str = "OCR service is currently unavailable. Please try again later."
|
|
OCR_SERVICE_CONFIG_ERROR: str = "OCR service configuration error. Please contact support."
|
|
OCR_UNEXPECTED_ERROR: str = "An unexpected error occurred during OCR processing."
|
|
OCR_QUOTA_EXCEEDED: str = "OCR service quota exceeded. Please try again later."
|
|
OCR_INVALID_FILE_TYPE: str = "Invalid file type. Supported types: {types}"
|
|
OCR_FILE_TOO_LARGE: str = "File too large. Maximum size: {size}MB"
|
|
OCR_PROCESSING_ERROR: str = "Error processing image: {detail}"
|
|
|
|
# --- Gemini AI Settings ---
|
|
GEMINI_MODEL_NAME: str = "gemini-2.0-flash" # The model to use for OCR
|
|
GEMINI_SAFETY_SETTINGS: dict = {
|
|
"HARM_CATEGORY_HATE_SPEECH": "BLOCK_MEDIUM_AND_ABOVE",
|
|
"HARM_CATEGORY_DANGEROUS_CONTENT": "BLOCK_MEDIUM_AND_ABOVE",
|
|
"HARM_CATEGORY_HARASSMENT": "BLOCK_MEDIUM_AND_ABOVE",
|
|
"HARM_CATEGORY_SEXUALLY_EXPLICIT": "BLOCK_MEDIUM_AND_ABOVE",
|
|
}
|
|
GEMINI_GENERATION_CONFIG: dict = {
|
|
"candidate_count": 1,
|
|
"max_output_tokens": 2048,
|
|
"temperature": 0.9,
|
|
"top_p": 1,
|
|
"top_k": 1
|
|
}
|
|
|
|
# --- API Settings ---
|
|
API_PREFIX: str = "/api" # Base path for all API endpoints
|
|
API_OPENAPI_URL: str = "/api/openapi.json"
|
|
API_DOCS_URL: str = "/api/docs"
|
|
API_REDOC_URL: str = "/api/redoc"
|
|
|
|
# CORS Origins - environment dependent
|
|
CORS_ORIGINS: str = "http://localhost:5173,http://localhost:5174,http://localhost:8000,http://127.0.0.1:5173,http://127.0.0.1:5174,http://127.0.0.1:8000"
|
|
FRONTEND_URL: str = "http://localhost:5173" # URL for the frontend application
|
|
|
|
# --- API Metadata ---
|
|
API_TITLE: str = "Shared Lists API"
|
|
API_DESCRIPTION: str = "API for managing shared shopping lists, OCR, and cost splitting."
|
|
API_VERSION: str = "0.1.0"
|
|
ROOT_MESSAGE: str = "Welcome to the Shared Lists API! Docs available at /api/docs"
|
|
|
|
# --- Logging Settings ---
|
|
LOG_LEVEL: str = "WARNING"
|
|
LOG_FORMAT: str = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
|
|
|
# --- Health Check Settings ---
|
|
HEALTH_STATUS_OK: str = "ok"
|
|
HEALTH_STATUS_ERROR: str = "error"
|
|
|
|
# --- HTTP Status Messages ---
|
|
HTTP_400_DETAIL: str = "Bad Request"
|
|
HTTP_401_DETAIL: str = "Unauthorized"
|
|
HTTP_403_DETAIL: str = "Forbidden"
|
|
HTTP_404_DETAIL: str = "Not Found"
|
|
HTTP_422_DETAIL: str = "Unprocessable Entity"
|
|
HTTP_429_DETAIL: str = "Too Many Requests"
|
|
HTTP_500_DETAIL: str = "Internal Server Error"
|
|
HTTP_503_DETAIL: str = "Service Unavailable"
|
|
|
|
# --- Database Error Messages ---
|
|
DB_CONNECTION_ERROR: str = "Database connection error"
|
|
DB_INTEGRITY_ERROR: str = "Database integrity error"
|
|
DB_TRANSACTION_ERROR: str = "Database transaction error"
|
|
DB_QUERY_ERROR: str = "Database query error"
|
|
|
|
# --- Auth Error Messages ---
|
|
AUTH_INVALID_CREDENTIALS: str = "Invalid username or password"
|
|
AUTH_NOT_AUTHENTICATED: str = "Not authenticated"
|
|
AUTH_JWT_ERROR: str = "JWT token error: {error}"
|
|
AUTH_JWT_UNEXPECTED_ERROR: str = "Unexpected JWT error: {error}"
|
|
AUTH_HEADER_NAME: str = "WWW-Authenticate"
|
|
AUTH_HEADER_PREFIX: str = "Bearer"
|
|
|
|
# OAuth Settings
|
|
# IMPORTANT: For Google OAuth to work, you MUST set the following environment variables
|
|
# (e.g., in your .env file):
|
|
# GOOGLE_CLIENT_ID: Your Google Cloud project's OAuth 2.0 Client ID
|
|
# GOOGLE_CLIENT_SECRET: Your Google Cloud project's OAuth 2.0 Client Secret
|
|
# Ensure the GOOGLE_REDIRECT_URI below matches the one configured in your Google Cloud Console.
|
|
GOOGLE_CLIENT_ID: str = ""
|
|
GOOGLE_CLIENT_SECRET: str = ""
|
|
GOOGLE_REDIRECT_URI: str = "http://localhost:8000/api/v1/auth/google/callback"
|
|
|
|
APPLE_CLIENT_ID: str = ""
|
|
APPLE_TEAM_ID: str = ""
|
|
APPLE_KEY_ID: str = ""
|
|
APPLE_PRIVATE_KEY: str = ""
|
|
APPLE_REDIRECT_URI: str = "http://localhost:8000/api/v1/auth/apple/callback"
|
|
|
|
# Session Settings
|
|
SESSION_SECRET_KEY: str = "your-session-secret-key" # Change this in production
|
|
ACCESS_TOKEN_EXPIRE_MINUTES: int = 480 # 8 hours instead of 30 minutes
|
|
|
|
# Redis Settings
|
|
REDIS_URL: str = "redis://localhost:6379"
|
|
REDIS_PASSWORD: str = ""
|
|
|
|
class Config:
|
|
env_file = ".env"
|
|
env_file_encoding = 'utf-8'
|
|
extra = "ignore"
|
|
|
|
@property
|
|
def cors_origins_list(self) -> List[str]:
|
|
"""Convert CORS_ORIGINS string to list"""
|
|
return [origin.strip() for origin in self.CORS_ORIGINS.split(",")]
|
|
|
|
@property
|
|
def is_production(self) -> bool:
|
|
"""Check if running in production environment"""
|
|
return self.ENVIRONMENT.lower() == "production"
|
|
|
|
@property
|
|
def is_development(self) -> bool:
|
|
"""Check if running in development environment"""
|
|
return self.ENVIRONMENT.lower() == "development"
|
|
|
|
@property
|
|
def docs_url(self) -> str | None:
|
|
"""Return docs URL only in development"""
|
|
return self.API_DOCS_URL if self.is_development else None
|
|
|
|
@property
|
|
def redoc_url(self) -> str | None:
|
|
"""Return redoc URL only in development"""
|
|
return self.API_REDOC_URL if self.is_development else None
|
|
|
|
@property
|
|
def openapi_url(self) -> str | None:
|
|
"""Return OpenAPI URL only in development"""
|
|
return self.API_OPENAPI_URL if self.is_development else None
|
|
|
|
settings = Settings()
|
|
|
|
# Validation for critical settings
|
|
if settings.DATABASE_URL is None:
|
|
raise ValueError("DATABASE_URL environment variable must be set.")
|
|
|
|
# Enforce secure secret key
|
|
if not settings.SECRET_KEY:
|
|
raise ValueError("SECRET_KEY environment variable must be set. Generate a secure key using: openssl rand -hex 32")
|
|
|
|
# Validate secret key strength
|
|
if len(settings.SECRET_KEY) < 32:
|
|
raise ValueError("SECRET_KEY must be at least 32 characters long for security")
|
|
|
|
# Production-specific validations
|
|
if settings.is_production:
|
|
if settings.SESSION_SECRET_KEY == "your-session-secret-key":
|
|
raise ValueError("SESSION_SECRET_KEY must be changed from default value in production")
|
|
|
|
if not settings.SENTRY_DSN:
|
|
logger.warning("SENTRY_DSN not set in production environment. Error tracking will be unavailable.")
|
|
|
|
if settings.GEMINI_API_KEY is None:
|
|
logger.error("CRITICAL: GEMINI_API_KEY environment variable not set. Gemini features will be unavailable.")
|
|
else:
|
|
# Optional: Log partial key for confirmation (avoid logging full key)
|
|
logger.info(f"GEMINI_API_KEY loaded (starts with: {settings.GEMINI_API_KEY[:4]}...).")
|
|
|
|
# Log environment information
|
|
logger.info(f"Application starting in {settings.ENVIRONMENT} environment")
|
|
if settings.is_production:
|
|
logger.info("Production mode: API documentation disabled")
|
|
else:
|
|
logger.info(f"Development mode: API documentation available at {settings.API_DOCS_URL}") |