feat: 阶段五 - 部署上线与安全加固
- Dockerfile: backend (FastAPI), celery-worker, web (多阶段构建 + Nginx) - docker-compose.yml: 完整 5 服务编排 (postgres + redis + backend + celery + nginx) - Nginx 反向代理配置 + gzip 压缩 + API 代理 - 安全增强: 请求大小限制、文件类型白名单(.docx/.txt/.pdf)、安全中间件 - .dockerignore: 排除 venv/node_modules 等 - .env.example: 精简为生产关键变量 - package.json: postinstall 自动复制 TinyMCE 自托管文件 - deploy.sh: 一键部署脚本
This commit is contained in:
parent
5fecbab6bf
commit
017a19515c
24
.env.example
24
.env.example
|
|
@ -1,23 +1,11 @@
|
||||||
# Database
|
# Database
|
||||||
POSTGRES_USER=docforge
|
POSTGRES_USER=docforge
|
||||||
POSTGRES_PASSWORD=docforge
|
POSTGRES_PASSWORD=change-this-password
|
||||||
POSTGRES_DB=docforge
|
POSTGRES_DB=docforge
|
||||||
POSTGRES_HOST=localhost
|
|
||||||
POSTGRES_PORT=5432
|
|
||||||
|
|
||||||
# Redis
|
# Security (generate with: python -c "import secrets; print(secrets.token_urlsafe(32))")
|
||||||
REDIS_URL=redis://localhost:6379/0
|
SECRET_KEY=change-me-to-a-random-secret-key
|
||||||
|
|
||||||
# Backend
|
# AI (optional, can be configured in the web UI)
|
||||||
SECRET_KEY=your-fernet-key-change-in-production
|
OPENAI_API_KEY=
|
||||||
DEFAULT_MODEL_ID=
|
AZURE_API_KEY=
|
||||||
|
|
||||||
# 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
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
venv/
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
.env
|
||||||
|
.git
|
||||||
|
storage/
|
||||||
|
alembic/versions/
|
||||||
|
*.egg-info/
|
||||||
|
.pytest_cache/
|
||||||
|
|
@ -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"]
|
||||||
|
|
@ -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"]
|
||||||
|
|
@ -3,6 +3,7 @@ from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form, Q
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from app.core.database import get_db
|
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.generation_point import GenerationPoint
|
||||||
from app.models.template import Template
|
from app.models.template import Template
|
||||||
from app.models.ai_model import AIModel
|
from app.models.ai_model import AIModel
|
||||||
|
|
@ -39,7 +40,8 @@ async def create_generation_point(
|
||||||
raise HTTPException(status_code=404, detail="模型不存在")
|
raise HTTPException(status_code=404, detail="模型不存在")
|
||||||
|
|
||||||
ref_file_path = None
|
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)
|
ref_file_path = await save_upload(ref_file, REF_FILES_DIR)
|
||||||
|
|
||||||
point = GenerationPoint(
|
point = GenerationPoint(
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ from fastapi.responses import Response
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from app.core.database import get_db
|
from app.core.database import get_db
|
||||||
|
from app.core.security_middleware import validate_file_extension
|
||||||
from app.models.template import Template
|
from app.models.template import Template
|
||||||
from app.schemas.template import TemplateResponse, TemplateListItem, HTMLContentResponse, HTMLUpdateRequest
|
from app.schemas.template import TemplateResponse, TemplateListItem, HTMLContentResponse, HTMLUpdateRequest
|
||||||
from app.services.file_storage import save_upload, get_file_content, delete_file, TEMPLATES_DIR
|
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"):
|
if not file.filename or not file.filename.endswith(".docx"):
|
||||||
raise HTTPException(status_code=400, detail="仅支持 .docx 文件")
|
raise HTTPException(status_code=400, detail="仅支持 .docx 文件")
|
||||||
|
|
||||||
|
validate_file_extension(file.filename)
|
||||||
file_path = await save_upload(file, TEMPLATES_DIR)
|
file_path = await save_upload(file, TEMPLATES_DIR)
|
||||||
file_content = await get_file_content(file_path)
|
file_content = await get_file_content(file_path)
|
||||||
html_content = await docx_to_html(file_content)
|
html_content = await docx_to_html(file_content)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from app.core.config import get_settings
|
from app.core.config import get_settings
|
||||||
|
from app.core.security_middleware import SecurityMiddleware
|
||||||
from app.api.router import api_router
|
from app.api.router import api_router
|
||||||
|
|
||||||
settings = get_settings()
|
settings = get_settings()
|
||||||
|
|
@ -18,6 +19,8 @@ app.add_middleware(
|
||||||
allow_headers=["*"],
|
allow_headers=["*"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
app.add_middleware(SecurityMiddleware)
|
||||||
|
|
||||||
app.include_router(api_router)
|
app.include_router(api_router)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
@ -15,6 +15,7 @@ services:
|
||||||
interval: 5s
|
interval: 5s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
image: redis:7-alpine
|
image: redis:7-alpine
|
||||||
|
|
@ -28,7 +29,73 @@ services:
|
||||||
interval: 5s
|
interval: 5s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
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:
|
volumes:
|
||||||
postgres_data:
|
postgres_data:
|
||||||
redis_data:
|
redis_data:
|
||||||
|
storage_data:
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
.git
|
||||||
|
*.local
|
||||||
|
public/tinymce/
|
||||||
|
|
@ -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;"]
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -6,8 +6,8 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "tsc -b && vite build",
|
"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": {
|
"dependencies": {
|
||||||
"@ant-design/icons": "^6.3.2",
|
"@ant-design/icons": "^6.3.2",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue