Files
yu-zhi-ran/platform/backend/app/api/articles.py
T
Yuzhiran Dev d601a26850 fix: API route security fixes (path traversal, auth, bare except)
- articles.py: Path traversal sanitization
- optimizer_logs.py: Admin auth guard
- platform_config.py: Admin auth guard
- system.py: Path traversal whitelist
- topic_config.py: Admin auth guard
- topics.py: Minor fix

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-06-16 08:23:50 +08:00

184 lines
7.7 KiB
Python

import re
from fastapi import APIRouter, HTTPException, Query, Depends, Body
from sqlalchemy.orm import Session
from sqlalchemy import or_
from pathlib import Path
import os
from datetime import datetime, date
from ..database import get_db
from ..models import User, Article, Topic
from .auth import get_current_user, org_filter
router = APIRouter(prefix="/api/articles", tags=["articles"])
@router.get("/drafts")
def list_drafts(topic_id: str = None, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)):
"""从 articles 表列出草稿"""
query = db.query(Article).join(Topic, Article.topic_id == Topic.id)
of = org_filter(current_user, Topic)
if of is not True:
query = query.filter(of)
if topic_id:
query = query.filter(Article.topic_id == topic_id)
articles = query.order_by(Article.created_at.desc()).all()
result = {}
for a in articles:
result.setdefault(a.platform, []).append({"id": a.id, "topic_id": a.topic_id, "status": a.status})
return {"articles": result}
@router.get("/list")
def list_articles(
platform: str = None,
status: str = None,
search: str = None,
topic_id: str = None,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""列出所有文章(含选题标题)"""
query = db.query(Article, Topic.title.label("topic_title")).join(Topic, Article.topic_id == Topic.id)
of = org_filter(current_user, Topic)
if of is not True:
query = query.filter(of)
if platform:
query = query.filter(Article.platform == platform)
if status:
query = query.filter(Article.status == status)
if topic_id:
query = query.filter(Article.topic_id == topic_id)
if search:
query = query.filter(
or_(Article.id.ilike(f"%{search}%"), Topic.title.ilike(f"%{search}%"))
)
rows = query.order_by(Article.created_at.desc()).all()
return {
"articles": [
{
"id": r.Article.id,
"topic_id": r.Article.topic_id,
"topic_title": r.topic_title,
"platform": r.Article.platform,
"status": r.Article.status,
"compliance_score": r.Article.compliance_score,
"word_count": r.Article.word_count,
"created_at": r.Article.created_at.isoformat() if r.Article.created_at else None,
"images": r.Article.images or {},
}
for r in rows
]
}
@router.get("/detail/{article_id}")
def get_article_detail(article_id: str, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)):
"""获取文章详情"""
article = db.query(Article).filter(Article.id == article_id).first()
if not article:
raise HTTPException(status_code=404, detail="Article not found")
topic = db.query(Topic).filter(Topic.id == article.topic_id).first()
if topic and current_user.role != "admin" and topic.org_id != current_user.org_id:
raise HTTPException(status_code=404, detail="Article not found")
return {
"id": article.id,
"topic_id": article.topic_id,
"topic_title": topic.title if topic else None,
"platform": article.platform,
"file_path": article.file_path,
"title": article.title,
"content": article.content,
"status": article.status,
"compliance_score": article.compliance_score,
"word_count": article.word_count,
"outline": article.outline,
"html_content": article.html_content,
"images": article.images or {},
"created_at": article.created_at.isoformat() if article.created_at else None,
}
@router.delete("/{article_id}")
def delete_article(article_id: str, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)):
"""删除文章"""
article = db.query(Article).filter(Article.id == article_id).first()
if not article:
raise HTTPException(status_code=404, detail="Article not found")
topic = db.query(Topic).filter(Topic.id == article.topic_id).first()
if topic and current_user.role != "admin" and topic.org_id != current_user.org_id:
raise HTTPException(status_code=404, detail="Article not found")
file_path = Path(article.file_path) if article.file_path else None
db.delete(article)
db.commit()
if file_path and file_path.exists():
try:
file_path.unlink()
except OSError:
pass
return {"detail": "deleted"}
@router.get("/{topic_id}/images")
def get_article_images(topic_id: str, platform: str = None, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)):
"""获取某选题在各平台的配图路径"""
topic = db.query(Topic).filter(Topic.id == topic_id).first()
if topic and current_user.role != "admin" and topic.org_id != current_user.org_id:
raise HTTPException(status_code=404, detail="Topic not found")
query = db.query(Article).filter(Article.topic_id == topic_id)
if platform:
query = query.filter(Article.platform == platform)
articles = query.all()
return {
topic_id: {
a.platform: (a.images or {})
for a in articles
}
}
@router.get("/{topic_id}/preview")
def preview_article(topic_id: str, platform: str = "zhihu", current_user: User = Depends(get_current_user), db: Session = Depends(get_db)):
"""从 articles 表预览某选题的 HTML 内容"""
topic = db.query(Topic).filter(Topic.id == topic_id).first()
if topic and current_user.role != "admin" and topic.org_id != current_user.org_id:
raise HTTPException(status_code=404, detail="Topic not found")
article_id = f"{platform}_{topic_id}"
article = db.query(Article).filter(Article.id == article_id).first()
if not article or not article.html_content:
raise HTTPException(status_code=404, detail=f"Article not found for {topic_id} on {platform}")
return {"topic_id": topic_id, "platform": platform, "html": article.html_content, "images": article.images or {}}
@router.put("/{topic_id}/content")
def update_article_content(
topic_id: str,
platform: str = Body(...),
html_content: str = Body(...),
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""保存用户编辑后的文章 HTML 内容"""
topic = db.query(Topic).filter(Topic.id == topic_id).first()
if topic and current_user.role != "admin" and topic.org_id != current_user.org_id:
raise HTTPException(status_code=404, detail="Topic not found")
article_id = f"{platform}_{topic_id}"
article = db.query(Article).filter(Article.id == article_id).first()
if not article:
article = Article(
id=article_id, topic_id=topic_id, platform=platform,
file_path=f"db:{article_id}", status="draft"
)
db.add(article)
article.html_content = html_content
db.commit()
return {"ok": True, "id": article_id}
@router.get("/optimization-report")
def get_optimization_report(publish_date: str = None, current_user: User = Depends(get_current_user)):
"""获取合规优化报告"""
if not publish_date:
publish_date = date.today().isoformat()
if not re.match(r'^\d{4}-\d{2}-\d{2}$', publish_date):
raise HTTPException(status_code=400, detail="Invalid date format (expected YYYY-MM-DD)")
PROJECT_ROOT = Path('/root/openclaw-workspace/projects/yu-zhi-ran')
report_path = PROJECT_ROOT / "automation" / "data" / "drafts" / publish_date / "optimization_report.json"
if not report_path.exists():
raise HTTPException(status_code=404, detail="No optimization report for this date")
report = report_path.read_text(encoding='utf-8')
import json
return json.loads(report)