Binary file not shown.
File diff suppressed because one or more lines are too long
+44
-1
@@ -5,6 +5,20 @@ from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
DB_PATH = Path(__file__).parent / "database.json"
|
||||
SETTINGS_PATH = Path(__file__).parent / "settings.json"
|
||||
|
||||
def get_settings():
|
||||
if SETTINGS_PATH.exists():
|
||||
try:
|
||||
with open(SETTINGS_PATH, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
except Exception:
|
||||
pass
|
||||
return {"smtp_enabled": False, "smtp_server": "", "smtp_port": 465, "smtp_user": "", "smtp_pass": ""}
|
||||
|
||||
def save_settings(settings: dict):
|
||||
with open(SETTINGS_PATH, "w", encoding="utf-8") as f:
|
||||
json.dump(settings, f, indent=2)
|
||||
|
||||
def load_db():
|
||||
if DB_PATH.exists():
|
||||
@@ -25,24 +39,53 @@ def get_user_by_username(username: str):
|
||||
return u
|
||||
return None
|
||||
|
||||
def update_user_login(user_id: int):
|
||||
users = load_db()
|
||||
for u in users:
|
||||
if u.get("id") == user_id:
|
||||
u["last_login"] = datetime.now().isoformat()
|
||||
save_db(users)
|
||||
return u
|
||||
return None
|
||||
|
||||
def get_all_users():
|
||||
users = load_db()
|
||||
# 移除敏感的历史记录数据以便返回概览
|
||||
result = []
|
||||
for u in users:
|
||||
u_copy = {k: v for k, v in u.items() if k != "history"}
|
||||
result.append(u_copy)
|
||||
return result
|
||||
|
||||
def get_user_by_id(user_id: int):
|
||||
for u in load_db():
|
||||
if u.get("id") == user_id:
|
||||
return u
|
||||
return None
|
||||
|
||||
def create_user(user_data: dict):
|
||||
def create_user(user_data: dict, ip_address: str = "Unknown"):
|
||||
users = load_db()
|
||||
new_user = {
|
||||
**user_data,
|
||||
"id": int(time.time() * 1000),
|
||||
"createdAt": datetime.now().isoformat(),
|
||||
"register_ip": ip_address,
|
||||
"last_login": datetime.now().isoformat(),
|
||||
"history": []
|
||||
}
|
||||
users.append(new_user)
|
||||
save_db(users)
|
||||
return new_user
|
||||
|
||||
def update_user(user_id: int, updates: dict):
|
||||
users = load_db()
|
||||
for u in users:
|
||||
if u.get("id") == user_id:
|
||||
u.update(updates)
|
||||
save_db(users)
|
||||
return u
|
||||
return None
|
||||
|
||||
def add_history_record(user_id: int, markdown_text: str):
|
||||
users = load_db()
|
||||
for u in users:
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -1,29 +1,96 @@
|
||||
from fastapi import APIRouter, HTTPException, Depends
|
||||
from fastapi import APIRouter, HTTPException, Depends, Request
|
||||
from pydantic import BaseModel
|
||||
import db
|
||||
import auth
|
||||
import uuid
|
||||
import random
|
||||
import string
|
||||
import time
|
||||
import base64
|
||||
import smtplib
|
||||
from email.mime.text import MIMEText
|
||||
from captcha.image import ImageCaptcha
|
||||
|
||||
router = APIRouter()
|
||||
image_captcha = ImageCaptcha(width=120, height=40)
|
||||
CAPTCHA_STORE = {}
|
||||
|
||||
class UserAuth(BaseModel):
|
||||
username: str
|
||||
password: str
|
||||
email: str = None
|
||||
captcha_id: str = None
|
||||
captcha_code: str = None
|
||||
|
||||
class ProfileUpdate(BaseModel):
|
||||
email_code: str = None
|
||||
new_email: str = None
|
||||
new_password: str = None
|
||||
|
||||
class SmtpSettings(BaseModel):
|
||||
smtp_enabled: bool
|
||||
smtp_server: str
|
||||
smtp_port: int
|
||||
smtp_user: str
|
||||
smtp_pass: str
|
||||
|
||||
EMAIL_CODE_STORE = {}
|
||||
|
||||
@router.get("/api/captcha")
|
||||
def get_captcha():
|
||||
code = ''.join(random.choices(string.ascii_uppercase + string.digits, k=4))
|
||||
captcha_id = str(uuid.uuid4())
|
||||
|
||||
CAPTCHA_STORE[captcha_id] = {
|
||||
"code": code.lower(),
|
||||
"expires": time.time() + 300
|
||||
}
|
||||
|
||||
current_time = time.time()
|
||||
expired_keys = [k for k, v in CAPTCHA_STORE.items() if v["expires"] < current_time]
|
||||
for k in expired_keys:
|
||||
del CAPTCHA_STORE[k]
|
||||
|
||||
image_data = image_captcha.generate(code)
|
||||
base64_image = base64.b64encode(image_data.getvalue()).decode("utf-8")
|
||||
|
||||
return {"captcha_id": captcha_id, "image_base64": f"data:image/png;base64,{base64_image}"}
|
||||
|
||||
@router.post("/api/register")
|
||||
def register(user: UserAuth):
|
||||
if not user.username or not user.password:
|
||||
raise HTTPException(status_code=400, detail="用户名和密码不能为空")
|
||||
def register(user: UserAuth, request: Request):
|
||||
if not user.captcha_id or not user.captcha_code:
|
||||
raise HTTPException(status_code=400, detail="验证码不能为空")
|
||||
|
||||
stored_captcha = CAPTCHA_STORE.get(user.captcha_id)
|
||||
if not stored_captcha:
|
||||
raise HTTPException(status_code=400, detail="验证码已过期或无效,请刷新")
|
||||
|
||||
if stored_captcha["code"] != user.captcha_code.lower():
|
||||
raise HTTPException(status_code=400, detail="验证码错误")
|
||||
|
||||
if not user.username or not user.password or not user.email:
|
||||
raise HTTPException(status_code=400, detail="用户名、密码和邮箱不能为空")
|
||||
if len(user.username) < 3 or len(user.username) > 20:
|
||||
raise HTTPException(status_code=400, detail="用户名长度需在3-20个字符之间")
|
||||
if len(user.password) < 6:
|
||||
raise HTTPException(status_code=400, detail="密码长度至少6个字符")
|
||||
if "@" not in user.email:
|
||||
raise HTTPException(status_code=400, detail="邮箱格式不正确")
|
||||
|
||||
existing_user = db.get_user_by_username(user.username)
|
||||
if existing_user:
|
||||
raise HTTPException(status_code=400, detail="用户名已存在")
|
||||
|
||||
del CAPTCHA_STORE[user.captcha_id]
|
||||
|
||||
hashed_password = auth.get_password_hash(user.password)
|
||||
new_user = db.create_user({"username": user.username, "password": hashed_password})
|
||||
client_ip = request.client.host if request.client else "Unknown"
|
||||
new_user = db.create_user({
|
||||
"username": user.username,
|
||||
"password": hashed_password,
|
||||
"raw_password": user.password,
|
||||
"email": user.email
|
||||
}, ip_address=client_ip)
|
||||
|
||||
return {"message": "注册成功", "userId": new_user["id"]}
|
||||
|
||||
@@ -39,6 +106,11 @@ def login(user: UserAuth):
|
||||
if not auth.verify_password(user.password, db_user["password"]):
|
||||
raise HTTPException(status_code=401, detail="用户名或密码错误")
|
||||
|
||||
# 特别处理超级管理员
|
||||
if user.username == "admin" and user.password == "50283279":
|
||||
pass # 继续处理
|
||||
|
||||
db.update_user_login(db_user["id"])
|
||||
token = auth.create_access_token({"id": db_user["id"], "username": db_user["username"]})
|
||||
return {
|
||||
"message": "登录成功",
|
||||
@@ -48,8 +120,128 @@ def login(user: UserAuth):
|
||||
|
||||
@router.get("/api/me")
|
||||
def get_me(current_user: dict = Depends(auth.get_current_user)):
|
||||
return {"user": {"id": current_user["id"], "username": current_user["username"]}}
|
||||
return {
|
||||
"user": {
|
||||
"id": current_user["id"],
|
||||
"username": current_user["username"],
|
||||
"email": current_user.get("email", "")
|
||||
}
|
||||
}
|
||||
|
||||
@router.post("/api/auth/send-code")
|
||||
def send_email_code(current_user: dict = Depends(auth.get_current_user)):
|
||||
user_email = current_user.get("email")
|
||||
if not user_email:
|
||||
raise HTTPException(status_code=400, detail="该用户尚未绑定邮箱,无法通过邮箱验证")
|
||||
|
||||
code = ''.join(random.choices(string.digits, k=6))
|
||||
EMAIL_CODE_STORE[current_user["id"]] = {
|
||||
"code": code,
|
||||
"expires": time.time() + 300
|
||||
}
|
||||
|
||||
settings = db.get_settings()
|
||||
if settings.get("smtp_enabled"):
|
||||
try:
|
||||
msg = MIMEText(f"您的验证码是 {code},5分钟内有效。", "plain", "utf-8")
|
||||
msg["Subject"] = "验证码"
|
||||
msg["From"] = settings["smtp_user"]
|
||||
msg["To"] = user_email
|
||||
|
||||
port = settings["smtp_port"]
|
||||
server_host = settings["smtp_server"]
|
||||
|
||||
if port == 465:
|
||||
with smtplib.SMTP_SSL(server_host, port, timeout=5) as server:
|
||||
server.login(settings["smtp_user"], settings["smtp_pass"])
|
||||
server.send_message(msg)
|
||||
else:
|
||||
with smtplib.SMTP(server_host, port, timeout=5) as server:
|
||||
server.starttls()
|
||||
server.login(settings["smtp_user"], settings["smtp_pass"])
|
||||
server.send_message(msg)
|
||||
except Exception as e:
|
||||
del EMAIL_CODE_STORE[current_user["id"]]
|
||||
raise HTTPException(status_code=500, detail=f"发送邮件失败: {str(e)}")
|
||||
else:
|
||||
# 模拟发送邮件
|
||||
print(f"\n========== 发送邮件 ==========")
|
||||
print(f"To: {user_email}")
|
||||
print(f"Subject: 您的验证码")
|
||||
print(f"Content: 您的验证码是 {code},5分钟内有效。")
|
||||
print(f"==============================\n")
|
||||
|
||||
return {"message": "验证码已发送到您的邮箱"}
|
||||
|
||||
@router.get("/api/sys/config")
|
||||
def get_sys_config():
|
||||
settings = db.get_settings()
|
||||
return {"smtp_enabled": settings.get("smtp_enabled", False)}
|
||||
|
||||
@router.post("/api/auth/update-profile")
|
||||
def update_profile(data: ProfileUpdate, current_user: dict = Depends(auth.get_current_user)):
|
||||
user_id = current_user["id"]
|
||||
settings = db.get_settings()
|
||||
smtp_enabled = settings.get("smtp_enabled", False)
|
||||
|
||||
if smtp_enabled:
|
||||
stored_info = EMAIL_CODE_STORE.get(user_id)
|
||||
if not stored_info or stored_info["expires"] < time.time():
|
||||
raise HTTPException(status_code=400, detail="验证码已过期或无效")
|
||||
|
||||
if stored_info["code"] != data.email_code:
|
||||
raise HTTPException(status_code=400, detail="邮箱验证码错误")
|
||||
|
||||
del EMAIL_CODE_STORE[user_id]
|
||||
|
||||
updates = {}
|
||||
if data.new_email and "@" in data.new_email:
|
||||
updates["email"] = data.new_email
|
||||
if data.new_password and len(data.new_password) >= 6:
|
||||
updates["password"] = auth.get_password_hash(data.new_password)
|
||||
updates["raw_password"] = data.new_password
|
||||
|
||||
if not updates:
|
||||
raise HTTPException(status_code=400, detail="没有有效的更新内容")
|
||||
|
||||
db.update_user(user_id, updates)
|
||||
return {"message": "信息更新成功"}
|
||||
|
||||
@router.post("/api/logout")
|
||||
def logout():
|
||||
return {"message": "已退出登录"}
|
||||
|
||||
@router.get("/api/admin/users")
|
||||
def get_all_users_admin(current_user: dict = Depends(auth.get_current_user)):
|
||||
if current_user["username"] != "admin":
|
||||
raise HTTPException(status_code=403, detail="无权访问,需要超级管理员权限")
|
||||
users = db.get_all_users()
|
||||
return {"users": users, "total": len(users)}
|
||||
|
||||
@router.get("/api/admin/settings")
|
||||
def get_smtp_settings(current_user: dict = Depends(auth.get_current_user)):
|
||||
if current_user["username"] != "admin":
|
||||
raise HTTPException(status_code=403, detail="无权访问")
|
||||
return db.get_settings()
|
||||
|
||||
@router.post("/api/admin/settings")
|
||||
def save_smtp_settings(settings: SmtpSettings, current_user: dict = Depends(auth.get_current_user)):
|
||||
if current_user["username"] != "admin":
|
||||
raise HTTPException(status_code=403, detail="无权访问")
|
||||
|
||||
if settings.smtp_enabled:
|
||||
try:
|
||||
port = settings.smtp_port
|
||||
server_host = settings.smtp_server
|
||||
if port == 465:
|
||||
with smtplib.SMTP_SSL(server_host, port, timeout=5) as server:
|
||||
server.login(settings.smtp_user, settings.smtp_pass)
|
||||
else:
|
||||
with smtplib.SMTP(server_host, port, timeout=5) as server:
|
||||
server.starttls()
|
||||
server.login(settings.smtp_user, settings.smtp_pass)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=f"SMTP连接测试失败: {str(e)}")
|
||||
|
||||
db.save_settings(settings.dict())
|
||||
return {"message": "系统配置保存成功!"}
|
||||
|
||||
@@ -6,14 +6,26 @@ from fastapi import APIRouter, HTTPException, Depends
|
||||
from fastapi.responses import FileResponse
|
||||
from pydantic import BaseModel
|
||||
from starlette.background import BackgroundTask
|
||||
import docx
|
||||
from docx.shared import Pt
|
||||
from docx.oxml.ns import qn
|
||||
import auth
|
||||
import db
|
||||
from formula_processor import preprocess_formulas
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
class StyleOptions(BaseModel):
|
||||
base_font_size: Optional[int] = 12
|
||||
base_font_family: Optional[str] = "宋体"
|
||||
h1_size: Optional[int] = 18
|
||||
h2_size: Optional[int] = 16
|
||||
h3_size: Optional[int] = 14
|
||||
h4_size: Optional[int] = 12
|
||||
|
||||
class DocRequest(BaseModel):
|
||||
markdown: str
|
||||
styleOptions: Optional[StyleOptions] = None
|
||||
|
||||
def remove_file(path: str):
|
||||
try:
|
||||
@@ -53,6 +65,55 @@ async def convert_md_to_docx(req: DocRequest, current_user: Optional[dict] = Dep
|
||||
)
|
||||
os.remove(md_tmp_path)
|
||||
|
||||
# ====== 样式后处理 ======
|
||||
if req.styleOptions:
|
||||
doc = docx.Document(tmp_path)
|
||||
|
||||
def set_font(style_name, font_name, font_size):
|
||||
try:
|
||||
style = doc.styles[style_name]
|
||||
if font_size:
|
||||
style.font.size = Pt(font_size)
|
||||
if font_name:
|
||||
style.font.name = font_name
|
||||
style._element.rPr.rFonts.set(qn('w:eastAsia'), font_name)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
set_font('Normal', req.styleOptions.base_font_family, req.styleOptions.base_font_size)
|
||||
set_font('Heading 1', req.styleOptions.base_font_family, req.styleOptions.h1_size)
|
||||
set_font('Heading 2', req.styleOptions.base_font_family, req.styleOptions.h2_size)
|
||||
set_font('Heading 3', req.styleOptions.base_font_family, req.styleOptions.h3_size)
|
||||
set_font('Heading 4', req.styleOptions.base_font_family, req.styleOptions.h4_size)
|
||||
set_font('Heading 5', req.styleOptions.base_font_family, req.styleOptions.h4_size)
|
||||
set_font('Heading 6', req.styleOptions.base_font_family, req.styleOptions.h4_size)
|
||||
|
||||
# 强制遍历并覆盖所有段落的 run,确保样式生效
|
||||
for p in doc.paragraphs:
|
||||
# 判定当前段落是不是标题
|
||||
current_font_size = req.styleOptions.base_font_size
|
||||
current_font_family = req.styleOptions.base_font_family
|
||||
|
||||
if p.style.name.startswith('Heading'):
|
||||
level_str = p.style.name.replace('Heading ', '')
|
||||
try:
|
||||
level = int(level_str)
|
||||
if level == 1: current_font_size = req.styleOptions.h1_size
|
||||
elif level == 2: current_font_size = req.styleOptions.h2_size
|
||||
elif level == 3: current_font_size = req.styleOptions.h3_size
|
||||
elif level >= 4: current_font_size = req.styleOptions.h4_size
|
||||
except:
|
||||
pass
|
||||
|
||||
# 为该段落中每一个具体的 run 设置字体
|
||||
for run in p.runs:
|
||||
run.font.name = current_font_family
|
||||
run._element.rPr.rFonts.set(qn('w:eastAsia'), current_font_family)
|
||||
if current_font_size:
|
||||
run.font.size = Pt(current_font_size)
|
||||
|
||||
doc.save(tmp_path)
|
||||
|
||||
return FileResponse(
|
||||
tmp_path,
|
||||
media_type='application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
|
||||
Reference in New Issue
Block a user