初始化
This commit is contained in:
1
backend/routes/__init__.py
Normal file
1
backend/routes/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""路由包"""
|
||||
61
backend/routes/import_api.py
Normal file
61
backend/routes/import_api.py
Normal file
@@ -0,0 +1,61 @@
|
||||
"""智能导入 API — 文本解析与匹配"""
|
||||
from fastapi import APIRouter, Depends, Request
|
||||
from sqlalchemy.orm import Session, joinedload
|
||||
from models import get_db, Website
|
||||
from middleware.user_context import UserContext
|
||||
from urllib.parse import urlparse
|
||||
|
||||
router = APIRouter(prefix="/api/import", tags=["智能导入"])
|
||||
|
||||
|
||||
@router.post("/parse")
|
||||
def parse_import_text(data: dict, db: Session = Depends(get_db), request: Request = None):
|
||||
"""解析粘贴的文本,返回结构化结果 + 匹配信息"""
|
||||
user_id = UserContext.get_user_id(request)
|
||||
text = data.get("text", "").strip()
|
||||
if not text:
|
||||
return {"ok": False, "error": "文本为空"}
|
||||
|
||||
lines = [l.strip() for l in text.split("\n") if l.strip()]
|
||||
if len(lines) < 3:
|
||||
return {"ok": False, "error": "至少需要 3 行:网址+网站名、账号、密码"}
|
||||
|
||||
# 解析首行: 网址 + 空格 + 网站名
|
||||
first_line = lines[0]
|
||||
space_idx = first_line.find(" ")
|
||||
if space_idx > 0:
|
||||
url = first_line[:space_idx].strip()
|
||||
site_name = first_line[space_idx + 1:].strip()
|
||||
else:
|
||||
url = first_line
|
||||
try:
|
||||
site_name = urlparse(url).hostname or url
|
||||
except Exception:
|
||||
site_name = url
|
||||
|
||||
result = {
|
||||
"ok": True,
|
||||
"url": url,
|
||||
"site_name": site_name,
|
||||
"username": lines[1] if len(lines) > 1 else "",
|
||||
"password": lines[2] if len(lines) > 2 else "",
|
||||
"remark": lines[3] if len(lines) > 3 else "",
|
||||
}
|
||||
|
||||
# 匹配已有网站(仅当前用户)
|
||||
existing = db.query(Website).options(joinedload(Website.accounts)).filter(
|
||||
Website.url == url, Website.created_by == user_id
|
||||
).first()
|
||||
if existing:
|
||||
result["matched_site"] = {
|
||||
"id": existing.id,
|
||||
"name": existing.name,
|
||||
"folder_id": existing.folder_id,
|
||||
"account_count": len(existing.accounts),
|
||||
}
|
||||
result["action"] = "append"
|
||||
else:
|
||||
result["matched_site"] = None
|
||||
result["action"] = "create"
|
||||
|
||||
return result
|
||||
159
backend/routes/notes.py
Normal file
159
backend/routes/notes.py
Normal file
@@ -0,0 +1,159 @@
|
||||
"""笔记相关路由"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
from sqlalchemy.orm import Session
|
||||
from models import get_db, Note
|
||||
from middleware.user_context import UserContext
|
||||
|
||||
router = APIRouter(tags=["笔记"])
|
||||
|
||||
|
||||
@router.get("/api/notes")
|
||||
def list_notes(folder_id: int = None, db: Session = Depends(get_db), request: Request = None):
|
||||
"""获取笔记列表(仅当前用户)"""
|
||||
user_id = UserContext.get_user_id(request)
|
||||
q = db.query(Note).filter(Note.created_by == user_id)
|
||||
if folder_id:
|
||||
q = q.filter(Note.folder_id == folder_id)
|
||||
return q.order_by(Note.updated_at.desc()).all()
|
||||
|
||||
|
||||
@router.post("/api/notes")
|
||||
def create_note(data: dict, db: Session = Depends(get_db), request: Request = None):
|
||||
"""创建笔记"""
|
||||
user_id = UserContext.get_user_id(request)
|
||||
note = Note(
|
||||
title=data.get("title", "无标题"),
|
||||
content=data.get("content", ""),
|
||||
folder_id=data.get("folder_id"),
|
||||
created_by=user_id,
|
||||
updated_by=user_id,
|
||||
)
|
||||
db.add(note)
|
||||
db.commit()
|
||||
db.refresh(note)
|
||||
return {"id": note.id, "title": note.title, "content": note.content,
|
||||
"folder_id": note.folder_id, "created_by": note.created_by, "updated_by": note.updated_by,
|
||||
"created_at": str(note.created_at), "updated_at": str(note.updated_at)}
|
||||
|
||||
|
||||
@router.get("/api/notes/{note_id}")
|
||||
def get_note(note_id: int, db: Session = Depends(get_db), request: Request = None):
|
||||
"""获取单个笔记"""
|
||||
user_id = UserContext.get_user_id(request)
|
||||
note = db.query(Note).filter(Note.id == note_id, Note.created_by == user_id).first()
|
||||
if not note:
|
||||
raise HTTPException(404, "笔记不存在")
|
||||
return {"id": note.id, "title": note.title, "content": note.content,
|
||||
"folder_id": note.folder_id, "created_by": note.created_by, "updated_by": note.updated_by,
|
||||
"created_at": str(note.created_at), "updated_at": str(note.updated_at)}
|
||||
|
||||
|
||||
@router.put("/api/notes/{note_id}")
|
||||
def update_note(note_id: int, data: dict, db: Session = Depends(get_db), request: Request = None):
|
||||
"""更新笔记"""
|
||||
user_id = UserContext.get_user_id(request)
|
||||
note = db.query(Note).filter(Note.id == note_id, Note.created_by == user_id).first()
|
||||
if not note:
|
||||
raise HTTPException(404, "笔记不存在")
|
||||
if "title" in data:
|
||||
note.title = data["title"]
|
||||
if "content" in data:
|
||||
note.content = data["content"]
|
||||
if "folder_id" in data:
|
||||
note.folder_id = data["folder_id"]
|
||||
note.updated_by = user_id
|
||||
db.commit()
|
||||
db.refresh(note)
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@router.delete("/api/notes/{note_id}")
|
||||
def delete_note(note_id: int, db: Session = Depends(get_db), request: Request = None):
|
||||
"""删除笔记"""
|
||||
user_id = UserContext.get_user_id(request)
|
||||
note = db.query(Note).filter(Note.id == note_id, Note.created_by == user_id).first()
|
||||
if not note:
|
||||
raise HTTPException(404, "笔记不存在")
|
||||
db.delete(note)
|
||||
db.commit()
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
# ===== 分享 =====
|
||||
import secrets
|
||||
|
||||
@router.post("/api/notes/{note_id}/share")
|
||||
def share_note(note_id: int, data: dict = {}, db: Session = Depends(get_db), request: Request = None):
|
||||
"""生成/刷新分享链接,可设置有效期"""
|
||||
from datetime import datetime, timedelta
|
||||
user_id = UserContext.get_user_id(request)
|
||||
note = db.query(Note).filter(Note.id == note_id, Note.created_by == user_id).first()
|
||||
if not note:
|
||||
raise HTTPException(404, "笔记不存在")
|
||||
note.share_token = secrets.token_urlsafe(16)
|
||||
|
||||
# 有效期: 7d / 30d / 1y / forever
|
||||
ttl = data.get("ttl", "forever")
|
||||
if ttl == "7d":
|
||||
note.share_expires_at = datetime.utcnow() + timedelta(days=7)
|
||||
elif ttl == "30d":
|
||||
note.share_expires_at = datetime.utcnow() + timedelta(days=30)
|
||||
elif ttl == "1y":
|
||||
note.share_expires_at = datetime.utcnow() + timedelta(days=365)
|
||||
else:
|
||||
note.share_expires_at = None # 永久
|
||||
|
||||
db.commit()
|
||||
return {
|
||||
"share_token": note.share_token,
|
||||
"url": f"/share/{note.share_token}",
|
||||
"expires_at": note.share_expires_at.strftime("%Y-%m-%d %H:%M") if note.share_expires_at else "永久有效",
|
||||
}
|
||||
|
||||
|
||||
@router.delete("/api/notes/{note_id}/share")
|
||||
def unshare_note(note_id: int, db: Session = Depends(get_db), request: Request = None):
|
||||
"""取消分享"""
|
||||
user_id = UserContext.get_user_id(request)
|
||||
note = db.query(Note).filter(Note.id == note_id, Note.created_by == user_id).first()
|
||||
if not note:
|
||||
raise HTTPException(404, "笔记不存在")
|
||||
note.share_token = None
|
||||
db.commit()
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
# ===== 移动 =====
|
||||
@router.put("/api/notes/{note_id}/move")
|
||||
def move_note(note_id: int, data: dict, db: Session = Depends(get_db), request: Request = None):
|
||||
"""移动笔记到另一个文件夹"""
|
||||
user_id = UserContext.get_user_id(request)
|
||||
note = db.query(Note).filter(Note.id == note_id, Note.created_by == user_id).first()
|
||||
if not note:
|
||||
raise HTTPException(404, "笔记不存在")
|
||||
note.folder_id = data.get("folder_id")
|
||||
db.commit()
|
||||
return {"ok": True, "folder_id": note.folder_id}
|
||||
|
||||
|
||||
# ===== 分享管理 =====
|
||||
@router.get("/api/shares")
|
||||
def list_shares(db: Session = Depends(get_db), request: Request = None):
|
||||
"""列出当前用户已分享的笔记"""
|
||||
user_id = UserContext.get_user_id(request)
|
||||
notes = db.query(Note).filter(
|
||||
Note.share_token.isnot(None),
|
||||
Note.created_by == user_id,
|
||||
).order_by(Note.updated_at.desc()).all()
|
||||
result = []
|
||||
for n in notes:
|
||||
result.append({
|
||||
"id": n.id,
|
||||
"title": n.title,
|
||||
"share_token": n.share_token,
|
||||
"share_url": f"/share/{n.share_token}",
|
||||
"share_expires_at": n.share_expires_at.strftime("%Y-%m-%d %H:%M") if n.share_expires_at else "永久有效",
|
||||
"created_by": n.created_by,
|
||||
"updated_at": str(n.updated_at),
|
||||
})
|
||||
return result
|
||||
75
backend/routes/share.py
Normal file
75
backend/routes/share.py
Normal file
@@ -0,0 +1,75 @@
|
||||
"""分享笔记公开查看"""
|
||||
from datetime import datetime
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi.responses import HTMLResponse
|
||||
from sqlalchemy.orm import Session
|
||||
from models import get_db, Note
|
||||
|
||||
router = APIRouter(tags=["分享"])
|
||||
|
||||
CSS = """
|
||||
:root{--text:#1F2329;--text2:#646A73;--text3:#8F959E;--border:#E5E6EB;--brand:#3370FF;--bg:#F5F6F8;}
|
||||
*{margin:0;padding:0;box-sizing:border-box;}
|
||||
body{font-family:-apple-system,BlinkMacSystemFont,"PingFang SC","Microsoft YaHei",sans-serif;color:var(--text);background:var(--bg);min-height:100vh;}
|
||||
.wrap{max-width:1100px;margin:0 auto;padding:32px 16px;}
|
||||
.card{background:#fff;border-radius:12px;padding:36px;box-shadow:0 1px 3px rgba(0,0,0,.06);overflow-x:auto;word-wrap:break-word;overflow-wrap:break-word;max-width:100%;}
|
||||
h1{font-size:26px;font-weight:700;padding-bottom:16px;margin-bottom:16px;border-bottom:2px solid var(--border);line-height:1.3;}
|
||||
.meta{font-size:12px;color:var(--text3);margin-bottom:28px;display:flex;gap:20px;}
|
||||
.content{font-size:15px;line-height:1.9;color:var(--text);overflow-x:auto;word-wrap:break-word;overflow-wrap:break-word;}
|
||||
.content h2{font-size:20px;font-weight:600;margin:24px 0 10px;}
|
||||
.content h3{font-size:17px;font-weight:600;margin:18px 0 8px;}
|
||||
.content p{margin:8px 0;}
|
||||
.content ul,.content ol{padding-left:22px;margin:8px 0;}
|
||||
.content li{margin:4px 0;}
|
||||
.content blockquote{border-left:3px solid var(--brand);padding:4px 14px;margin:12px 0;color:var(--text2);}
|
||||
.content pre{background:#F7F8FA;padding:14px 18px;border-radius:8px;overflow-x:auto;font-size:13px;line-height:1.7;font-family:"SF Mono","Fira Code","Consolas",monospace;}
|
||||
.content code{background:#F2F3F5;padding:2px 6px;border-radius:3px;font-size:13px;font-family:"SF Mono","Fira Code","Consolas",monospace;}
|
||||
.content pre code{background:none;padding:0;}
|
||||
.content table{border-collapse:collapse;width:100%;margin:12px 0;}
|
||||
.content th,.content td{border:1px solid var(--border);padding:8px 14px;text-align:left;}
|
||||
.content th{background:#F7F8FA;font-weight:600;font-size:13px;}
|
||||
.content img{max-width:100%;border-radius:8px;}
|
||||
.content mark{background:#FFF3CD;padding:2px 4px;border-radius:2px;}
|
||||
.footer{text-align:center;margin-top:24px;font-size:11px;color:var(--text3);}
|
||||
.footer a{color:var(--brand);text-decoration:none;}
|
||||
"""
|
||||
|
||||
TEMPLATE = """<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<title>{title} - MoKeeText</title>
|
||||
<style>{css}</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<div class="card">
|
||||
<h1>{title}</h1>
|
||||
<div class="meta">
|
||||
<span>创建于 {created_at}</span>
|
||||
<span>{created_by}</span>
|
||||
</div>
|
||||
<div class="content">{content}</div>
|
||||
</div>
|
||||
<div class="footer">Powered by <a href="/">MoKeeText</a></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
|
||||
@router.get("/share/{token}")
|
||||
def view_shared(token: str, db: Session = Depends(get_db)):
|
||||
note = db.query(Note).filter_by(share_token=token).first()
|
||||
if not note:
|
||||
raise HTTPException(404, "链接无效或已取消分享")
|
||||
if note.share_expires_at and note.share_expires_at < datetime.utcnow():
|
||||
raise HTTPException(410, "分享链接已过期")
|
||||
|
||||
html = TEMPLATE.format(
|
||||
title=note.title,
|
||||
created_at=note.created_at.strftime('%Y-%m-%d %H:%M:%S') if note.created_at else '-',
|
||||
created_by=note.created_by or 'admin',
|
||||
content=note.content or '<p style="color:var(--text3);">暂无内容</p>',
|
||||
css=CSS,
|
||||
)
|
||||
return HTMLResponse(html)
|
||||
101
backend/routes/upload.py
Normal file
101
backend/routes/upload.py
Normal file
@@ -0,0 +1,101 @@
|
||||
"""文件上传/读取 — 数据库存储(file_service.py 为可选备用方案,保留不删)"""
|
||||
from fastapi import APIRouter, UploadFile, File, Depends, HTTPException, Request
|
||||
from fastapi.responses import Response
|
||||
from sqlalchemy.orm import Session
|
||||
from logger import logger
|
||||
from models import get_db, Upload
|
||||
from middleware.user_context import UserContext
|
||||
# 文件服务模块保留备用(不删除),如需切换取消下面注释即可
|
||||
# from file_service import upload_file, download_file, delete_file
|
||||
|
||||
router = APIRouter(tags=["上传"])
|
||||
|
||||
ALLOWED = {"image/png", "image/jpeg", "image/gif", "image/webp", "image/svg+xml"}
|
||||
MAX_SIZE = 20 * 1024 * 1024 # 20MB
|
||||
|
||||
|
||||
@router.post("/api/upload/image")
|
||||
async def upload_image(file: UploadFile = File(...), db: Session = Depends(get_db), request: Request = None):
|
||||
"""上传图片 — 存入数据库 LONGBLOB"""
|
||||
user_id = UserContext.get_user_id(request)
|
||||
try:
|
||||
if file.content_type not in ALLOWED:
|
||||
raise HTTPException(400, f"不支持的图片格式: {file.content_type}")
|
||||
data = await file.read()
|
||||
if len(data) > MAX_SIZE:
|
||||
raise HTTPException(400, f"图片不能超过 20MB,当前 {len(data)} 字节")
|
||||
|
||||
filename = file.filename or "img"
|
||||
logger.info(f"上传图片: {filename}, {len(data)} bytes, {file.content_type}, user={user_id}")
|
||||
|
||||
upload = Upload(
|
||||
filename=filename,
|
||||
content_type=file.content_type,
|
||||
data=data,
|
||||
size=len(data),
|
||||
created_by=user_id,
|
||||
)
|
||||
db.add(upload)
|
||||
db.commit()
|
||||
db.refresh(upload)
|
||||
|
||||
logger.info(f"图片已存入 DB, id={upload.id}")
|
||||
return {"url": f"/api/file/{upload.id}"}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"上传失败: {e}")
|
||||
raise HTTPException(500, f"上传失败: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/api/file/{file_id}")
|
||||
def get_file(file_id: int, db: Session = Depends(get_db)):
|
||||
"""读取图片 — 公开访问(不走网关,供编辑器 <img> 标签和分享页加载)
|
||||
|
||||
优先从 DB blob 读取;如为文件服务存储则代理下载
|
||||
"""
|
||||
upload = db.query(Upload).filter_by(id=file_id).first()
|
||||
if not upload:
|
||||
raise HTTPException(404, "文件不存在")
|
||||
|
||||
# DB blob(主存储方式)
|
||||
if upload.data:
|
||||
return Response(content=upload.data, media_type=upload.content_type)
|
||||
|
||||
# 文件服务存储(兼容之前上传的文件)
|
||||
if upload.file_id:
|
||||
from file_service import download_file
|
||||
try:
|
||||
data, content_type, _ = download_file(upload.file_id)
|
||||
return Response(content=data, media_type=content_type)
|
||||
except FileNotFoundError:
|
||||
raise HTTPException(404, "文件在文件服务中不存在")
|
||||
except Exception as e:
|
||||
logger.error(f"从文件服务下载失败: {e}")
|
||||
raise HTTPException(500, f"下载文件失败: {str(e)}")
|
||||
|
||||
raise HTTPException(404, "文件数据不存在")
|
||||
|
||||
|
||||
@router.delete("/api/file/{file_id}")
|
||||
def delete_file_api(file_id: int, db: Session = Depends(get_db), request: Request = None):
|
||||
"""删除图片 — 从 DB 和文件服务中删除"""
|
||||
user_id = UserContext.get_user_id(request)
|
||||
upload = db.query(Upload).filter_by(id=file_id).first()
|
||||
if not upload:
|
||||
raise HTTPException(404, "文件不存在")
|
||||
|
||||
# 如果存在文件服务记录,同步删除
|
||||
if upload.file_id:
|
||||
from file_service import delete_file
|
||||
try:
|
||||
delete_file(upload.file_id)
|
||||
logger.info(f"文件服务删除成功, fileId={upload.file_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"文件服务删除失败: {e}")
|
||||
|
||||
db.delete(upload)
|
||||
db.commit()
|
||||
logger.info(f"图片记录已删除, db_id={file_id}, user={user_id}")
|
||||
return {"ok": True}
|
||||
221
backend/routes/websites.py
Normal file
221
backend/routes/websites.py
Normal file
@@ -0,0 +1,221 @@
|
||||
"""网站管理相关路由"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
from sqlalchemy.orm import Session, joinedload
|
||||
from models import get_db, Website, Account, Folder
|
||||
from middleware.user_context import UserContext
|
||||
|
||||
router = APIRouter(tags=["网站管理"])
|
||||
|
||||
|
||||
# ===== 网站 CRUD =====
|
||||
|
||||
@router.get("/api/websites")
|
||||
def list_websites(folder_id: int = None, db: Session = Depends(get_db), request: Request = None):
|
||||
"""获取网站列表(仅当前用户)"""
|
||||
user_id = UserContext.get_user_id(request)
|
||||
q = db.query(Website).options(joinedload(Website.accounts)).filter(Website.created_by == user_id)
|
||||
if folder_id:
|
||||
q = q.filter(Website.folder_id == folder_id)
|
||||
return q.order_by(Website.name).all()
|
||||
|
||||
|
||||
@router.post("/api/websites")
|
||||
def create_website(data: dict, db: Session = Depends(get_db), request: Request = None):
|
||||
"""创建网站"""
|
||||
user_id = UserContext.get_user_id(request)
|
||||
website = Website(
|
||||
name=data.get("name", ""),
|
||||
url=data.get("url", ""),
|
||||
folder_id=data.get("folder_id"),
|
||||
created_by=user_id,
|
||||
updated_by=user_id,
|
||||
)
|
||||
db.add(website)
|
||||
db.commit()
|
||||
db.refresh(website)
|
||||
return {"id": website.id, "name": website.name, "url": website.url,
|
||||
"folder_id": website.folder_id}
|
||||
|
||||
|
||||
@router.get("/api/websites/{website_id}")
|
||||
def get_website(website_id: int, db: Session = Depends(get_db), request: Request = None):
|
||||
"""获取网站详情(含账号列表)"""
|
||||
user_id = UserContext.get_user_id(request)
|
||||
website = db.query(Website).options(joinedload(Website.accounts)).filter(
|
||||
Website.id == website_id, Website.created_by == user_id
|
||||
).first()
|
||||
if not website:
|
||||
raise HTTPException(404, "网站不存在")
|
||||
return website
|
||||
|
||||
|
||||
@router.put("/api/websites/{website_id}")
|
||||
def update_website(website_id: int, data: dict, db: Session = Depends(get_db), request: Request = None):
|
||||
"""更新网站信息"""
|
||||
user_id = UserContext.get_user_id(request)
|
||||
website = db.query(Website).filter(Website.id == website_id, Website.created_by == user_id).first()
|
||||
if not website:
|
||||
raise HTTPException(404, "网站不存在")
|
||||
for field in ["name", "url", "folder_id"]:
|
||||
if field in data:
|
||||
setattr(website, field, data[field])
|
||||
website.updated_by = user_id
|
||||
db.commit()
|
||||
db.refresh(website)
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@router.delete("/api/websites/{website_id}")
|
||||
def delete_website(website_id: int, db: Session = Depends(get_db), request: Request = None):
|
||||
"""删除网站"""
|
||||
user_id = UserContext.get_user_id(request)
|
||||
website = db.query(Website).filter(Website.id == website_id, Website.created_by == user_id).first()
|
||||
if not website:
|
||||
raise HTTPException(404, "网站不存在")
|
||||
db.delete(website)
|
||||
db.commit()
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
# ===== 账号 CRUD =====
|
||||
|
||||
@router.post("/api/websites/{website_id}/accounts")
|
||||
def add_account(website_id: int, data: dict, db: Session = Depends(get_db), request: Request = None):
|
||||
"""添加账号"""
|
||||
user_id = UserContext.get_user_id(request)
|
||||
account = Account(
|
||||
website_id=website_id,
|
||||
username=data.get("username", ""),
|
||||
password=data.get("password", ""),
|
||||
remark=data.get("remark", ""),
|
||||
sort_order=data.get("sort_order", 0),
|
||||
created_by=user_id,
|
||||
updated_by=user_id,
|
||||
)
|
||||
db.add(account)
|
||||
db.commit()
|
||||
db.refresh(account)
|
||||
return {"id": account.id, "username": account.username, "remark": account.remark, "sort_order": account.sort_order}
|
||||
|
||||
|
||||
@router.get("/api/accounts/{account_id}")
|
||||
def get_account(account_id: int, db: Session = Depends(get_db), request: Request = None):
|
||||
"""获取单个账号"""
|
||||
user_id = UserContext.get_user_id(request)
|
||||
account = db.query(Account).filter(Account.id == account_id, Account.created_by == user_id).first()
|
||||
if not account:
|
||||
raise HTTPException(404, "账号不存在")
|
||||
return {"id": account.id, "username": account.username,
|
||||
"password": account.password, "remark": account.remark}
|
||||
|
||||
|
||||
@router.put("/api/accounts/{account_id}")
|
||||
def update_account(account_id: int, data: dict, db: Session = Depends(get_db), request: Request = None):
|
||||
"""更新账号"""
|
||||
user_id = UserContext.get_user_id(request)
|
||||
account = db.query(Account).filter(Account.id == account_id, Account.created_by == user_id).first()
|
||||
if not account:
|
||||
raise HTTPException(404, "账号不存在")
|
||||
for field in ["username", "password", "remark", "sort_order"]:
|
||||
if field in data:
|
||||
setattr(account, field, data[field])
|
||||
account.updated_by = user_id
|
||||
db.commit()
|
||||
db.refresh(account)
|
||||
return {"ok": True}
|
||||
|
||||
@router.delete("/api/accounts/{account_id}")
|
||||
def delete_account(account_id: int, db: Session = Depends(get_db), request: Request = None):
|
||||
"""删除账号"""
|
||||
user_id = UserContext.get_user_id(request)
|
||||
account = db.query(Account).filter(Account.id == account_id, Account.created_by == user_id).first()
|
||||
if not account:
|
||||
raise HTTPException(404, "账号不存在")
|
||||
db.delete(account)
|
||||
db.commit()
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@router.put("/api/accounts/{account_id}/move")
|
||||
def move_account(account_id: int, data: dict, db: Session = Depends(get_db), request: Request = None):
|
||||
"""移动账号顺序: direction = 'up' | 'down'"""
|
||||
user_id = UserContext.get_user_id(request)
|
||||
account = db.query(Account).filter(Account.id == account_id, Account.created_by == user_id).first()
|
||||
if not account:
|
||||
raise HTTPException(404, "账号不存在")
|
||||
|
||||
direction = data.get("direction", "down")
|
||||
website_id = account.website_id
|
||||
|
||||
# 同网站下按 sort_order 排序的所有账号
|
||||
siblings = db.query(Account).filter_by(website_id=website_id).order_by(Account.sort_order).all()
|
||||
|
||||
idx = next((i for i, a in enumerate(siblings) if a.id == account_id), None)
|
||||
if idx is None:
|
||||
return {"ok": False}
|
||||
|
||||
if direction == "up" and idx > 0:
|
||||
siblings[idx].sort_order, siblings[idx-1].sort_order = siblings[idx-1].sort_order, siblings[idx].sort_order
|
||||
elif direction == "down" and idx < len(siblings) - 1:
|
||||
siblings[idx].sort_order, siblings[idx+1].sort_order = siblings[idx+1].sort_order, siblings[idx].sort_order
|
||||
|
||||
db.commit()
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
# ===== 文件夹 API =====
|
||||
|
||||
@router.get("/api/folders")
|
||||
def list_folders(type: str = "website", db: Session = Depends(get_db), request: Request = None):
|
||||
"""获取文件夹列表(仅当前用户)"""
|
||||
user_id = UserContext.get_user_id(request)
|
||||
return db.query(Folder).filter(
|
||||
Folder.type == type,
|
||||
Folder.created_by == user_id,
|
||||
).order_by(Folder.sort_order).all()
|
||||
|
||||
|
||||
@router.post("/api/folders")
|
||||
def create_folder(data: dict, db: Session = Depends(get_db), request: Request = None):
|
||||
"""创建文件夹"""
|
||||
user_id = UserContext.get_user_id(request)
|
||||
folder = Folder(
|
||||
name=data.get("name", "新文件夹"),
|
||||
type=data.get("type", "website"),
|
||||
created_by=user_id,
|
||||
)
|
||||
db.add(folder)
|
||||
db.commit()
|
||||
db.refresh(folder)
|
||||
return {"id": folder.id, "name": folder.name, "type": folder.type}
|
||||
|
||||
|
||||
@router.put("/api/folders/{folder_id}")
|
||||
def update_folder(folder_id: int, data: dict, db: Session = Depends(get_db), request: Request = None):
|
||||
"""重命名文件夹"""
|
||||
user_id = UserContext.get_user_id(request)
|
||||
folder = db.query(Folder).filter(Folder.id == folder_id, Folder.created_by == user_id).first()
|
||||
if not folder:
|
||||
raise HTTPException(404, "文件夹不存在")
|
||||
if "name" in data:
|
||||
folder.name = data["name"]
|
||||
db.commit()
|
||||
return {"ok": True, "id": folder.id, "name": folder.name}
|
||||
|
||||
|
||||
@router.delete("/api/folders/{folder_id}")
|
||||
def delete_folder(folder_id: int, db: Session = Depends(get_db), request: Request = None):
|
||||
"""删除文件夹(文件夹下的内容变为未分类)"""
|
||||
user_id = UserContext.get_user_id(request)
|
||||
folder = db.query(Folder).filter(Folder.id == folder_id, Folder.created_by == user_id).first()
|
||||
if not folder:
|
||||
raise HTTPException(404, "文件夹不存在")
|
||||
# 将该文件夹下的网站/笔记移到未分类(仅限当前用户)
|
||||
from models import Website, Note
|
||||
if folder.type == "website":
|
||||
db.query(Website).filter_by(folder_id=folder_id, created_by=user_id).update({"folder_id": None})
|
||||
else:
|
||||
db.query(Note).filter_by(folder_id=folder_id, created_by=user_id).update({"folder_id": None})
|
||||
db.delete(folder)
|
||||
db.commit()
|
||||
return {"ok": True}
|
||||
Reference in New Issue
Block a user