mirror of
https://github.com/handsomezhuzhu/QQuiz.git
synced 2026-02-20 12:00:14 +00:00
安全修复和管理员账号密码自定义
This commit is contained in:
@@ -10,6 +10,9 @@ DATABASE_URL=sqlite+aiosqlite:///./qquiz.db
|
|||||||
# JWT Secret (must be at least 32 characters; generate randomly for production)
|
# JWT Secret (must be at least 32 characters; generate randomly for production)
|
||||||
SECRET_KEY=
|
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)
|
# Default admin password (must be at least 12 characters; generate randomly for production)
|
||||||
ADMIN_PASSWORD=
|
ADMIN_PASSWORD=
|
||||||
|
|
||||||
|
|||||||
@@ -86,6 +86,11 @@ async def init_default_config(db: AsyncSession):
|
|||||||
"ai_provider": os.getenv("AI_PROVIDER", "openai"),
|
"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")
|
admin_password = os.getenv("ADMIN_PASSWORD")
|
||||||
if not admin_password or len(admin_password) < 12:
|
if not admin_password or len(admin_password) < 12:
|
||||||
raise ValueError("ADMIN_PASSWORD must be set and at least 12 characters long")
|
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)
|
db.add(config)
|
||||||
print(f"✅ Created default config: {key} = {value}")
|
print(f"✅ Created default config: {key} = {value}")
|
||||||
|
|
||||||
# Create default admin user if not exists
|
# Create or update default admin user
|
||||||
result = await db.execute(select(User).where(User.username == "admin"))
|
result = await db.execute(select(User).where(User.username == admin_username))
|
||||||
admin = result.scalar_one_or_none()
|
admin = result.scalar_one_or_none()
|
||||||
|
|
||||||
default_admin_id = admin.id if admin else None
|
default_admin_id = admin.id if admin else None
|
||||||
|
|
||||||
if not admin:
|
if not admin:
|
||||||
admin_user = User(
|
admin_user = User(
|
||||||
username="admin",
|
username=admin_username,
|
||||||
hashed_password=pwd_context.hash(admin_password),
|
hashed_password=pwd_context.hash(admin_password),
|
||||||
is_admin=True
|
is_admin=True
|
||||||
)
|
)
|
||||||
@@ -115,8 +120,12 @@ async def init_default_config(db: AsyncSession):
|
|||||||
await db.commit()
|
await db.commit()
|
||||||
await db.refresh(admin_user)
|
await db.refresh(admin_user)
|
||||||
default_admin_id = admin_user.id
|
default_admin_id = admin_user.id
|
||||||
print("✅ Created default admin user (username: admin)")
|
print(f"✅ Created default admin user (username: {admin_username})")
|
||||||
else:
|
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()
|
await db.commit()
|
||||||
|
|
||||||
if default_admin_id is not None:
|
if default_admin_id is not None:
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
Authentication Router
|
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.ext.asyncio import AsyncSession
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
@@ -66,6 +66,7 @@ async def register(
|
|||||||
@router.post("/login", response_model=Token)
|
@router.post("/login", response_model=Token)
|
||||||
@limiter.limit("5/minute")
|
@limiter.limit("5/minute")
|
||||||
async def login(
|
async def login(
|
||||||
|
request: Request,
|
||||||
user_data: UserLogin,
|
user_data: UserLogin,
|
||||||
db: AsyncSession = Depends(get_db)
|
db: AsyncSession = Depends(get_db)
|
||||||
):
|
):
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
Exam Router - Handles exam creation, file upload, and deduplication
|
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 fastapi.responses import StreamingResponse
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from sqlalchemy import select, func, and_
|
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)
|
@router.post("/create", response_model=ExamUploadResponse, status_code=status.HTTP_201_CREATED)
|
||||||
@limiter.limit("10/minute")
|
@limiter.limit("10/minute")
|
||||||
async def create_exam_with_upload(
|
async def create_exam_with_upload(
|
||||||
|
request: Request,
|
||||||
background_tasks: BackgroundTasks,
|
background_tasks: BackgroundTasks,
|
||||||
title: str = Form(...),
|
title: str = Form(...),
|
||||||
file: UploadFile = File(...),
|
file: UploadFile = File(...),
|
||||||
@@ -587,6 +588,7 @@ async def create_exam_with_upload(
|
|||||||
@router.post("/{exam_id}/append", response_model=ExamUploadResponse)
|
@router.post("/{exam_id}/append", response_model=ExamUploadResponse)
|
||||||
@limiter.limit("10/minute")
|
@limiter.limit("10/minute")
|
||||||
async def append_document_to_exam(
|
async def append_document_to_exam(
|
||||||
|
request: Request,
|
||||||
exam_id: int,
|
exam_id: int,
|
||||||
background_tasks: BackgroundTasks,
|
background_tasks: BackgroundTasks,
|
||||||
file: UploadFile = File(...),
|
file: UploadFile = File(...),
|
||||||
|
|||||||
@@ -11,41 +11,12 @@ services:
|
|||||||
container_name: qquiz
|
container_name: qquiz
|
||||||
ports:
|
ports:
|
||||||
- "8000:8000"
|
- "8000:8000"
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
environment:
|
environment:
|
||||||
# 数据库配置(SQLite 默认)
|
# 数据库配置(SQLite 默认,使用持久化卷)
|
||||||
- DATABASE_URL=sqlite+aiosqlite:////app/data/qquiz.db
|
- 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:
|
volumes:
|
||||||
# 持久化数据卷
|
# 持久化数据卷
|
||||||
- qquiz_data:/app/data # 数据库文件
|
- qquiz_data:/app/data # 数据库文件
|
||||||
|
|||||||
Reference in New Issue
Block a user