fix: 端口更换 & 代码审核修复
端口: - 服务端口 8000 → 7329 - 前端开发端口 5173 → 7330 安全: - CORS 收紧为白名单,关闭 credentials - SPA 路由白名单完善 - 前端 XSS 转义 可靠性: - 时区统一为 datetime.now(timezone.utc) - 文章入库改为内存去重 + 增量计数 - OPML 导入改为 body 参数接收 - OPML 导出 URL XML 转义 - 首次抓取改为 BackgroundTasks 异步 - articles.py HTTPException 移到顶部 import - FTS5 异常显式日志 - FTS5 查询加引号包裹防布尔注入 - 中文摘要支持中文标点 - 去掉未使用的 hashlib import 部署: - Dockerfile 锁 python:3.12.7-slim - requirements 锁定具体版本 - healthcheck 不用 curl(镜像里没有) - docker-compose 使用 .env 文件 - 新增 .env 配置文件
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
"""RSS 源管理 API"""
|
||||
from typing import List, Optional
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks
|
||||
from pydantic import BaseModel, HttpUrl
|
||||
from sqlalchemy.orm import Session
|
||||
from database import get_db
|
||||
@@ -103,7 +103,11 @@ def list_categories(db: Session = Depends(get_db)):
|
||||
|
||||
|
||||
@router.post("", response_model=dict)
|
||||
def create_feed(data: FeedCreate, db: Session = Depends(get_db)):
|
||||
def create_feed(
|
||||
data: FeedCreate,
|
||||
background_tasks: BackgroundTasks,
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""添加 RSS 源"""
|
||||
# 检查是否已存在
|
||||
existing = db.query(Feed).filter(Feed.url == data.url).first()
|
||||
@@ -126,10 +130,10 @@ def create_feed(data: FeedCreate, db: Session = Depends(get_db)):
|
||||
if feed.is_active:
|
||||
add_feed_job(feed.id, feed.fetch_interval_minutes)
|
||||
|
||||
# 立即抓取一次
|
||||
fetch_and_store_feed(feed.id)
|
||||
# 后台异步首次抓取,不阻塞 HTTP 响应
|
||||
background_tasks.add_task(fetch_and_store_feed, feed.id)
|
||||
|
||||
return {"id": feed.id, "message": "RSS 源添加成功", "url": feed.url}
|
||||
return {"id": feed.id, "message": "RSS 源添加成功,正在后台抓取", "url": feed.url}
|
||||
|
||||
|
||||
@router.post("/discover")
|
||||
@@ -217,13 +221,25 @@ def trigger_fetch(feed_id: int, db: Session = Depends(get_db)):
|
||||
return result
|
||||
|
||||
|
||||
class OpmlImport(BaseModel):
|
||||
opml_content: str
|
||||
|
||||
|
||||
@router.post("/import-opml")
|
||||
def import_opml(opml_content: str, db: Session = Depends(get_db)):
|
||||
def import_opml(data: OpmlImport, db: Session = Depends(get_db)):
|
||||
"""导入 OPML 文件内容"""
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
content = data.opml_content.strip()
|
||||
if not content:
|
||||
raise HTTPException(status_code=400, detail="OPML 内容不能为空")
|
||||
|
||||
# 限制大小(防止滥用)
|
||||
if len(content) > 5_000_000: # 5MB
|
||||
raise HTTPException(status_code=413, detail="OPML 文件过大")
|
||||
|
||||
try:
|
||||
root = ET.fromstring(opml_content)
|
||||
root = ET.fromstring(content)
|
||||
except ET.ParseError:
|
||||
raise HTTPException(status_code=400, detail="无效的 OPML 文件")
|
||||
|
||||
@@ -261,12 +277,14 @@ def import_opml(opml_content: str, db: Session = Depends(get_db)):
|
||||
@router.get("/export-opml")
|
||||
def export_opml(db: Session = Depends(get_db)):
|
||||
"""导出 OPML 文件内容"""
|
||||
from xml.sax.saxutils import escape
|
||||
feeds = db.query(Feed).all()
|
||||
|
||||
lines = ['<?xml version="1.0" encoding="UTF-8"?>', '<opml version="2.0">', '<head><title>rssKeeper Feeds</title></head>', '<body>']
|
||||
for feed in feeds:
|
||||
title = (feed.title or feed.url).replace('"', '"')
|
||||
lines.append(f' <outline type="rss" text="{title}" xmlUrl="{feed.url}" />')
|
||||
title = escape(feed.title or feed.url, {'"': '"'})
|
||||
url = escape(feed.url)
|
||||
lines.append(f' <outline type="rss" text="{title}" xmlUrl="{url}" />')
|
||||
lines.append('</body>')
|
||||
lines.append('</opml>')
|
||||
|
||||
|
||||
Reference in New Issue
Block a user