diff --git a/.env.example b/.env.example index 9479d6b..f551ac2 100644 --- a/.env.example +++ b/.env.example @@ -1,23 +1,11 @@ # Database POSTGRES_USER=docforge -POSTGRES_PASSWORD=docforge +POSTGRES_PASSWORD=change-this-password POSTGRES_DB=docforge -POSTGRES_HOST=localhost -POSTGRES_PORT=5432 -# Redis -REDIS_URL=redis://localhost:6379/0 +# Security (generate with: python -c "import secrets; print(secrets.token_urlsafe(32))") +SECRET_KEY=change-me-to-a-random-secret-key -# Backend -SECRET_KEY=your-fernet-key-change-in-production -DEFAULT_MODEL_ID= - -# File Storage -STORAGE_ROOT=./storage - -# Celery -CELERY_BROKER_URL=redis://localhost:6379/0 -CELERY_RESULT_BACKEND=redis://localhost:6379/1 - -# CORS -CORS_ORIGINS=http://localhost:3000,http://localhost:5173 +# AI (optional, can be configured in the web UI) +OPENAI_API_KEY= +AZURE_API_KEY= diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 0000000..8514800 --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,9 @@ +venv/ +__pycache__/ +*.pyc +.env +.git +storage/ +alembic/versions/ +*.egg-info/ +.pytest_cache/ diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..26f9c4d --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,21 @@ +FROM python:3.12-slim + +WORKDIR /app + +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 + +RUN apt-get update && apt-get install -y --no-install-recommends \ + gcc libpq-dev && \ + rm -rf /var/lib/apt/lists/* + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +RUN mkdir -p /app/storage/templates /app/storage/ref_files /app/storage/results + +EXPOSE 8000 + +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/backend/Dockerfile.celery b/backend/Dockerfile.celery new file mode 100644 index 0000000..2a897c9 --- /dev/null +++ b/backend/Dockerfile.celery @@ -0,0 +1,19 @@ +FROM python:3.12-slim + +WORKDIR /app + +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 + +RUN apt-get update && apt-get install -y --no-install-recommends \ + gcc libpq-dev && \ + rm -rf /var/lib/apt/lists/* + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +RUN mkdir -p /app/storage/templates /app/storage/ref_files /app/storage/results + +CMD ["celery", "-A", "app.tasks.celery_app", "worker", "--loglevel=info", "--concurrency=4"] diff --git a/backend/app/api/generation_points.py b/backend/app/api/generation_points.py index 8f60e50..aa2c6e6 100644 --- a/backend/app/api/generation_points.py +++ b/backend/app/api/generation_points.py @@ -3,6 +3,7 @@ from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form, Q from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select from app.core.database import get_db +from app.core.security_middleware import validate_file_extension from app.models.generation_point import GenerationPoint from app.models.template import Template from app.models.ai_model import AIModel @@ -39,7 +40,8 @@ async def create_generation_point( raise HTTPException(status_code=404, detail="模型不存在") ref_file_path = None - if ref_file: + if ref_file and ref_file.filename: + validate_file_extension(ref_file.filename) ref_file_path = await save_upload(ref_file, REF_FILES_DIR) point = GenerationPoint( diff --git a/backend/app/api/templates.py b/backend/app/api/templates.py index 5c085ab..593163d 100644 --- a/backend/app/api/templates.py +++ b/backend/app/api/templates.py @@ -4,6 +4,7 @@ from fastapi.responses import Response from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select from app.core.database import get_db +from app.core.security_middleware import validate_file_extension from app.models.template import Template from app.schemas.template import TemplateResponse, TemplateListItem, HTMLContentResponse, HTMLUpdateRequest from app.services.file_storage import save_upload, get_file_content, delete_file, TEMPLATES_DIR @@ -21,6 +22,7 @@ async def upload_template( if not file.filename or not file.filename.endswith(".docx"): raise HTTPException(status_code=400, detail="仅支持 .docx 文件") + validate_file_extension(file.filename) file_path = await save_upload(file, TEMPLATES_DIR) file_content = await get_file_content(file_path) html_content = await docx_to_html(file_content) diff --git a/backend/app/core/security_middleware.py b/backend/app/core/security_middleware.py new file mode 100644 index 0000000..c104559 --- /dev/null +++ b/backend/app/core/security_middleware.py @@ -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 diff --git a/backend/app/main.py b/backend/app/main.py index 2ac7adb..5ca2be1 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -1,6 +1,7 @@ from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from app.core.config import get_settings +from app.core.security_middleware import SecurityMiddleware from app.api.router import api_router settings = get_settings() @@ -18,6 +19,8 @@ app.add_middleware( allow_headers=["*"], ) +app.add_middleware(SecurityMiddleware) + app.include_router(api_router) diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 0000000..cb5c564 --- /dev/null +++ b/deploy.sh @@ -0,0 +1,34 @@ +#!/bin/bash +set -e + +echo ">>> 启动 Doc Forge Reds <<<" + +if ! command -v docker &> /dev/null; then + echo "错误: 需要安装 Docker" + exit 1 +fi + +if [ ! -f .env ]; then + echo "请先复制 .env.example 为 .env 并填写必要配置" + exit 1 +fi + +echo "1. 构建镜像..." +docker compose build + +echo "2. 启动服务..." +docker compose up -d + +echo "3. 等待数据库就绪..." +sleep 5 + +echo "4. 执行数据库迁移..." +docker compose exec backend alembic upgrade head + +echo "" +echo ">>> 启动完成 <<<" +echo "前端地址: http://localhost:3000" +echo "后端文档: http://localhost:8000/docs" +echo "" +echo "查看日志: docker compose logs -f" +echo "停止服务: docker compose down" diff --git a/docker-compose.yml b/docker-compose.yml index a17752d..db4729f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,6 +15,7 @@ services: interval: 5s timeout: 5s retries: 5 + restart: unless-stopped redis: image: redis:7-alpine @@ -28,7 +29,73 @@ services: interval: 5s timeout: 5s retries: 5 + restart: unless-stopped + + backend: + build: + context: ./backend + dockerfile: Dockerfile + container_name: docforge-backend + environment: + POSTGRES_USER: ${POSTGRES_USER:-docforge} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-docforge} + POSTGRES_DB: ${POSTGRES_DB:-docforge} + POSTGRES_HOST: postgres + POSTGRES_PORT: 5432 + REDIS_URL: redis://redis:6379/0 + CELERY_BROKER_URL: redis://redis:6379/0 + CELERY_RESULT_BACKEND: redis://redis:6379/1 + SECRET_KEY: ${SECRET_KEY} + STORAGE_ROOT: /app/storage + CORS_ORIGINS: '["http://localhost:3000","http://localhost:80","http://localhost"]' + ports: + - "8000:8000" + volumes: + - storage_data:/app/storage + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + restart: unless-stopped + + celery-worker: + build: + context: ./backend + dockerfile: Dockerfile.celery + container_name: docforge-celery + environment: + POSTGRES_USER: ${POSTGRES_USER:-docforge} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-docforge} + POSTGRES_DB: ${POSTGRES_DB:-docforge} + POSTGRES_HOST: postgres + POSTGRES_PORT: 5432 + REDIS_URL: redis://redis:6379/0 + CELERY_BROKER_URL: redis://redis:6379/0 + CELERY_RESULT_BACKEND: redis://redis:6379/1 + SECRET_KEY: ${SECRET_KEY} + STORAGE_ROOT: /app/storage + volumes: + - storage_data:/app/storage + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + restart: unless-stopped + + nginx: + build: + context: ./web + dockerfile: Dockerfile + container_name: docforge-nginx + ports: + - "3000:80" + depends_on: + - backend + restart: unless-stopped volumes: postgres_data: redis_data: + storage_data: diff --git a/web/.dockerignore b/web/.dockerignore new file mode 100644 index 0000000..565674f --- /dev/null +++ b/web/.dockerignore @@ -0,0 +1,5 @@ +node_modules/ +dist/ +.git +*.local +public/tinymce/ diff --git a/web/Dockerfile b/web/Dockerfile new file mode 100644 index 0000000..b845f90 --- /dev/null +++ b/web/Dockerfile @@ -0,0 +1,18 @@ +FROM node:22-alpine AS build + +WORKDIR /app + +COPY package.json package-lock.json ./ +RUN npm ci + +COPY . . +RUN npm run postinstall && npm run build + +FROM nginx:alpine + +COPY --from=build /app/dist /usr/share/nginx/html +COPY nginx.conf /etc/nginx/conf.d/default.conf + +EXPOSE 80 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/web/nginx.conf b/web/nginx.conf new file mode 100644 index 0000000..acccc3e --- /dev/null +++ b/web/nginx.conf @@ -0,0 +1,26 @@ +server { + listen 80; + server_name _; + + client_max_body_size 100M; + + root /usr/share/nginx/html; + index index.html; + + location /api/ { + proxy_pass http://backend:8000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 300s; + } + + location / { + try_files $uri $uri/ /index.html; + } + + gzip on; + gzip_types text/plain text/css application/json application/javascript text/xml application/xml; + gzip_min_length 1000; +} diff --git a/web/package.json b/web/package.json index 68f8e73..b6d78de 100644 --- a/web/package.json +++ b/web/package.json @@ -6,8 +6,8 @@ "scripts": { "dev": "vite", "build": "tsc -b && vite build", - "lint": "oxlint", - "preview": "vite preview" + "preview": "vite preview", + "postinstall": "cp -r node_modules/tinymce public/tinymce 2>/dev/null || true" }, "dependencies": { "@ant-design/icons": "^6.3.2",