init: 初始化项目
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
from pydantic_settings import BaseSettings
|
||||
from functools import lru_cache
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
PROJECT_NAME: str = "Doc Forge Reborn"
|
||||
API_V1_PREFIX: str = "/api/v1"
|
||||
DEBUG: bool = False
|
||||
|
||||
POSTGRES_USER: str = "docforge"
|
||||
POSTGRES_PASSWORD: str = "docforge"
|
||||
POSTGRES_DB: str = "docforge"
|
||||
POSTGRES_HOST: str = "localhost"
|
||||
POSTGRES_PORT: int = 5432
|
||||
|
||||
REDIS_URL: str = "redis://localhost:6379/0"
|
||||
CELERY_BROKER_URL: str = "redis://localhost:6379/0"
|
||||
CELERY_RESULT_BACKEND: str = "redis://localhost:6379/1"
|
||||
|
||||
SECRET_KEY: str = "change-me-in-production"
|
||||
DEFAULT_MODEL_ID: str = ""
|
||||
|
||||
STORAGE_ROOT: str = "./storage"
|
||||
MAX_UPLOAD_SIZE: int = 50 * 1024 * 1024
|
||||
|
||||
CORS_ORIGINS: list[str] = ["http://localhost:3000", "http://localhost:5173"]
|
||||
|
||||
@property
|
||||
def DATABASE_URL(self) -> str:
|
||||
return (
|
||||
f"postgresql+asyncpg://{self.POSTGRES_USER}:{self.POSTGRES_PASSWORD}"
|
||||
f"@{self.POSTGRES_HOST}:{self.POSTGRES_PORT}/{self.POSTGRES_DB}"
|
||||
)
|
||||
|
||||
@property
|
||||
def DATABASE_URL_SYNC(self) -> str:
|
||||
return (
|
||||
f"postgresql://{self.POSTGRES_USER}:{self.POSTGRES_PASSWORD}"
|
||||
f"@{self.POSTGRES_HOST}:{self.POSTGRES_PORT}/{self.POSTGRES_DB}"
|
||||
)
|
||||
|
||||
model_config = {"env_file": ".env", "env_file_encoding": "utf-8", "case_sensitive": True}
|
||||
|
||||
|
||||
@lru_cache
|
||||
def get_settings() -> Settings:
|
||||
return Settings()
|
||||
@@ -0,0 +1,35 @@
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
|
||||
from sqlalchemy.orm import DeclarativeBase
|
||||
from app.core.config import get_settings
|
||||
|
||||
settings = get_settings()
|
||||
|
||||
engine = create_async_engine(
|
||||
settings.DATABASE_URL,
|
||||
echo=settings.DEBUG,
|
||||
pool_size=20,
|
||||
max_overflow=10,
|
||||
pool_pre_ping=True,
|
||||
)
|
||||
|
||||
async_session_factory = async_sessionmaker(
|
||||
engine,
|
||||
class_=AsyncSession,
|
||||
expire_on_commit=False,
|
||||
)
|
||||
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
pass
|
||||
|
||||
|
||||
async def get_db() -> AsyncSession:
|
||||
async with async_session_factory() as session:
|
||||
try:
|
||||
yield session
|
||||
await session.commit()
|
||||
except Exception:
|
||||
await session.rollback()
|
||||
raise
|
||||
finally:
|
||||
await session.close()
|
||||
@@ -0,0 +1,25 @@
|
||||
import base64
|
||||
import hashlib
|
||||
from cryptography.fernet import Fernet
|
||||
from app.core.config import get_settings
|
||||
|
||||
|
||||
def _derive_fernet_key(secret: str) -> bytes:
|
||||
digest = hashlib.sha256(secret.encode("utf-8")).digest()
|
||||
return base64.urlsafe_b64encode(digest)
|
||||
|
||||
|
||||
def _get_fernet() -> Fernet:
|
||||
settings = get_settings()
|
||||
key = _derive_fernet_key(settings.SECRET_KEY)
|
||||
return Fernet(key)
|
||||
|
||||
|
||||
def encrypt_api_key(api_key: str) -> str:
|
||||
f = _get_fernet()
|
||||
return f.encrypt(api_key.encode("utf-8")).decode("utf-8")
|
||||
|
||||
|
||||
def decrypt_api_key(encrypted_key: str) -> str:
|
||||
f = _get_fernet()
|
||||
return f.decrypt(encrypted_key.encode("utf-8")).decode("utf-8")
|
||||
@@ -0,0 +1,32 @@
|
||||
from fastapi import Request, HTTPException
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
from app.core.config import get_settings
|
||||
|
||||
settings = get_settings()
|
||||
|
||||
|
||||
class SecurityMiddleware(BaseHTTPMiddleware):
|
||||
async def dispatch(self, request: Request, call_next):
|
||||
if request.method in ("POST", "PUT", "PATCH"):
|
||||
content_length = request.headers.get("content-length")
|
||||
if content_length and int(content_length) > settings.MAX_UPLOAD_SIZE:
|
||||
raise HTTPException(
|
||||
status_code=413,
|
||||
detail=f"文件大小不能超过 {settings.MAX_UPLOAD_SIZE // 1024 // 1024}MB",
|
||||
)
|
||||
response = await call_next(request)
|
||||
return response
|
||||
|
||||
|
||||
ALLOWED_EXTENSIONS = {".docx", ".txt", ".pdf"}
|
||||
|
||||
|
||||
def validate_file_extension(filename: str) -> str:
|
||||
import os
|
||||
ext = os.path.splitext(filename)[1].lower()
|
||||
if ext not in ALLOWED_EXTENSIONS:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"不支持的文件类型: {ext},允许的类型: {', '.join(ALLOWED_EXTENSIONS)}",
|
||||
)
|
||||
return ext
|
||||
Reference in New Issue
Block a user