Document secure secrets and prune unused assets

This commit is contained in:
Simon
2025-12-13 01:35:56 +08:00
parent c4bb32b163
commit 1adf30d476
28 changed files with 157 additions and 1451 deletions

View File

@@ -24,6 +24,18 @@ router = APIRouter()
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
async def get_default_admin_id(db: AsyncSession) -> Optional[int]:
result = await db.execute(
select(SystemConfig).where(SystemConfig.key == "default_admin_id")
)
config = result.scalar_one_or_none()
if config and config.value.isdigit():
return int(config.value)
return None
@router.get("/config", response_model=SystemConfigResponse)
async def get_system_config(
current_admin: User = Depends(get_current_admin_user),
@@ -203,8 +215,10 @@ async def update_user(
detail="User not found"
)
protected_admin_id = await get_default_admin_id(db)
# 不允许修改默认管理员的管理员状态
if user.username == "admin" and user_data.is_admin is not None:
if protected_admin_id and user.id == protected_admin_id and user_data.is_admin is not None:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Cannot modify default admin user's admin status"
@@ -240,8 +254,10 @@ async def delete_user(
detail="User not found"
)
protected_admin_id = await get_default_admin_id(db)
# 不允许删除默认管理员
if user.username == "admin":
if protected_admin_id and user.id == protected_admin_id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Cannot delete default admin user"

View File

@@ -5,14 +5,17 @@ from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from datetime import timedelta
import logging
from database import get_db
from models import User, SystemConfig
from schemas import UserCreate, UserLogin, Token, UserResponse
from utils import hash_password, verify_password, create_access_token
from rate_limit import limiter
from services.auth_service import get_current_user
router = APIRouter()
logger = logging.getLogger(__name__)
@router.post("/register", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
@@ -61,6 +64,7 @@ async def register(
@router.post("/login", response_model=Token)
@limiter.limit("5/minute")
async def login(
user_data: UserLogin,
db: AsyncSession = Depends(get_db)
@@ -86,8 +90,7 @@ async def login(
data={"sub": str(user.id)} # JWT 'sub' must be a string
)
print(f"Login successful: user={user.username}, id={user.id}")
print(f"🔑 Generated token (first 50 chars): {access_token[:50]}...")
logger.info("Login successful", extra={"user_id": user.id})
return {
"access_token": access_token,

View File

@@ -10,6 +10,7 @@ from datetime import datetime, timedelta
import os
import aiofiles
import json
import magic
from database import get_db
from models import User, Exam, Question, ExamStatus, SystemConfig
@@ -24,8 +25,57 @@ from services.config_service import load_llm_config
from services.progress_service import progress_service
from utils import is_allowed_file, calculate_content_hash
from dedup_utils import is_duplicate_question
from rate_limit import limiter
router = APIRouter()
ALLOWED_MIME_TYPES = {
"text/plain",
"application/pdf",
"application/msword",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"application/vnd.ms-excel",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
}
async def validate_upload_file(file: UploadFile) -> None:
"""Validate uploaded file by extension and MIME type."""
if not file.filename or not is_allowed_file(file.filename):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid file type. Allowed: txt, pdf, doc, docx, xlsx, xls",
)
try:
sample = await file.read(2048)
except Exception:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Could not read uploaded file for validation",
)
if not sample:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Uploaded file is empty",
)
try:
mime_type = magic.from_buffer(sample, mime=True)
except Exception:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Could not determine file type",
)
finally:
file.file.seek(0)
if mime_type not in ALLOWED_MIME_TYPES:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid file content type",
)
async def check_upload_limits(user_id: int, file_size: int, db: AsyncSession):
@@ -485,6 +535,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(
background_tasks: BackgroundTasks,
title: str = Form(...),
@@ -498,11 +549,7 @@ async def create_exam_with_upload(
"""
# Validate file
if not file.filename or not is_allowed_file(file.filename):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid file type. Allowed: txt, pdf, doc, docx, xlsx, xls"
)
await validate_upload_file(file)
# Read file content
file_content = await file.read()
@@ -538,6 +585,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(
exam_id: int,
background_tasks: BackgroundTasks,
@@ -572,11 +620,7 @@ async def append_document_to_exam(
)
# Validate file
if not file.filename or not is_allowed_file(file.filename):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid file type. Allowed: txt, pdf, doc, docx, xlsx, xls"
)
await validate_upload_file(file)
# Read file content
file_content = await file.read()