diff --git a/.env.example b/.env.example index 9fdc178..250549f 100644 --- a/.env.example +++ b/.env.example @@ -10,6 +10,9 @@ DATABASE_URL=sqlite+aiosqlite:///./qquiz.db # JWT Secret (must be at least 32 characters; generate randomly for production) SECRET_KEY= +# Default admin username (must be at least 3 characters; default: admin) +ADMIN_USERNAME=admin + # Default admin password (must be at least 12 characters; generate randomly for production) ADMIN_PASSWORD= diff --git a/backend/database.py b/backend/database.py index 70f7058..9cca092 100644 --- a/backend/database.py +++ b/backend/database.py @@ -86,6 +86,11 @@ async def init_default_config(db: AsyncSession): "ai_provider": os.getenv("AI_PROVIDER", "openai"), } + # Validate admin credentials + admin_username = os.getenv("ADMIN_USERNAME", "admin") + if not admin_username or len(admin_username) < 3: + raise ValueError("ADMIN_USERNAME must be at least 3 characters long") + admin_password = os.getenv("ADMIN_PASSWORD") if not admin_password or len(admin_password) < 12: raise ValueError("ADMIN_PASSWORD must be set and at least 12 characters long") @@ -99,15 +104,15 @@ async def init_default_config(db: AsyncSession): db.add(config) print(f"✅ Created default config: {key} = {value}") - # Create default admin user if not exists - result = await db.execute(select(User).where(User.username == "admin")) + # Create or update default admin user + result = await db.execute(select(User).where(User.username == admin_username)) admin = result.scalar_one_or_none() default_admin_id = admin.id if admin else None if not admin: admin_user = User( - username="admin", + username=admin_username, hashed_password=pwd_context.hash(admin_password), is_admin=True ) @@ -115,8 +120,12 @@ async def init_default_config(db: AsyncSession): await db.commit() await db.refresh(admin_user) default_admin_id = admin_user.id - print("✅ Created default admin user (username: admin)") + print(f"✅ Created default admin user (username: {admin_username})") else: + # Update password if it has changed (verify current password doesn't match) + if not pwd_context.verify(admin_password, admin.hashed_password): + admin.hashed_password = pwd_context.hash(admin_password) + print(f"🔄 Updated default admin password (username: {admin_username})") await db.commit() if default_admin_id is not None: diff --git a/backend/routers/auth.py b/backend/routers/auth.py index 50ac970..d58dd72 100644 --- a/backend/routers/auth.py +++ b/backend/routers/auth.py @@ -1,7 +1,7 @@ """ Authentication Router """ -from fastapi import APIRouter, Depends, HTTPException, status +from fastapi import APIRouter, Depends, HTTPException, status, Request from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select from datetime import timedelta @@ -66,6 +66,7 @@ async def register( @router.post("/login", response_model=Token) @limiter.limit("5/minute") async def login( + request: Request, user_data: UserLogin, db: AsyncSession = Depends(get_db) ): diff --git a/backend/routers/exam.py b/backend/routers/exam.py index 54a9860..f03ee0d 100644 --- a/backend/routers/exam.py +++ b/backend/routers/exam.py @@ -1,7 +1,7 @@ """ Exam Router - Handles exam creation, file upload, and deduplication """ -from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File, Form, BackgroundTasks +from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File, Form, BackgroundTasks, Request from fastapi.responses import StreamingResponse from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, func, and_ @@ -537,6 +537,7 @@ async def async_parse_and_save( @router.post("/create", response_model=ExamUploadResponse, status_code=status.HTTP_201_CREATED) @limiter.limit("10/minute") async def create_exam_with_upload( + request: Request, background_tasks: BackgroundTasks, title: str = Form(...), file: UploadFile = File(...), @@ -587,6 +588,7 @@ async def create_exam_with_upload( @router.post("/{exam_id}/append", response_model=ExamUploadResponse) @limiter.limit("10/minute") async def append_document_to_exam( + request: Request, exam_id: int, background_tasks: BackgroundTasks, file: UploadFile = File(...), diff --git a/docker-compose-single.yml b/docker-compose-single.yml index 2362cc7..17a096d 100644 --- a/docker-compose-single.yml +++ b/docker-compose-single.yml @@ -11,41 +11,12 @@ services: container_name: qquiz ports: - "8000:8000" + env_file: + - .env environment: - # 数据库配置(SQLite 默认) + # 数据库配置(SQLite 默认,使用持久化卷) - DATABASE_URL=sqlite+aiosqlite:////app/data/qquiz.db - # JWT 密钥(生产环境必须设置为随机字符串) - - SECRET_KEY=${SECRET_KEY:?Set SECRET_KEY to a random string of at least 32 characters} - - # 管理员密码(生产环境必须设置为随机强密码,至少 12 位) - - ADMIN_PASSWORD=${ADMIN_PASSWORD:?Set ADMIN_PASSWORD to a strong password of at least 12 characters} - - # AI 提供商配置 - - AI_PROVIDER=gemini - - GEMINI_API_KEY=${GEMINI_API_KEY} - - GEMINI_BASE_URL=${GEMINI_BASE_URL:-https://generativelanguage.googleapis.com} - - GEMINI_MODEL=${GEMINI_MODEL:-gemini-2.0-flash-exp} - - # OpenAI 配置(可选) - - OPENAI_API_KEY=${OPENAI_API_KEY:-} - - OPENAI_BASE_URL=${OPENAI_BASE_URL:-https://api.openai.com/v1} - - OPENAI_MODEL=${OPENAI_MODEL:-gpt-4o-mini} - - # Anthropic 配置(可选) - - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:-} - - ANTHROPIC_MODEL=${ANTHROPIC_MODEL:-claude-3-haiku-20240307} - - # Qwen 配置(可选) - - QWEN_API_KEY=${QWEN_API_KEY:-} - - QWEN_BASE_URL=${QWEN_BASE_URL:-https://dashscope.aliyuncs.com/compatible-mode/v1} - - QWEN_MODEL=${QWEN_MODEL:-qwen-plus} - - # 系统配置 - - ALLOW_REGISTRATION=true - - MAX_UPLOAD_SIZE_MB=10 - - MAX_DAILY_UPLOADS=20 - volumes: # 持久化数据卷 - qquiz_data:/app/data # 数据库文件