v1.0.0提交
This commit is contained in:
24
backend/routers/v1/__init__.py
Normal file
24
backend/routers/v1/__init__.py
Normal file
@@ -0,0 +1,24 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
PerToolBox Server - API v1 路由汇总
|
||||
Copyright (C) 2024 Sea Network Technology Studio
|
||||
Author: Canglan <admin@sea-studio.top>
|
||||
License: AGPL v3
|
||||
"""
|
||||
|
||||
from .auth import router as auth_router
|
||||
from .user import router as user_router
|
||||
from .todos import router as todos_router
|
||||
from .notes import router as notes_router
|
||||
from .tools import router as tools_router
|
||||
from .stats import router as stats_router
|
||||
|
||||
__all__ = [
|
||||
"auth_router",
|
||||
"user_router",
|
||||
"todos_router",
|
||||
"notes_router",
|
||||
"tools_router",
|
||||
"stats_router"
|
||||
]
|
||||
167
backend/routers/v1/auth.py
Normal file
167
backend/routers/v1/auth.py
Normal file
@@ -0,0 +1,167 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
PerToolBox Server - 认证路由
|
||||
Copyright (C) 2024 Sea Network Technology Studio
|
||||
Author: Canglan <admin@sea-studio.top>
|
||||
License: AGPL v3
|
||||
"""
|
||||
|
||||
import re
|
||||
from fastapi import APIRouter, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
from ...dependencies import DbDependency
|
||||
from ...models import User
|
||||
from ...schemas import (
|
||||
SendCodeRequest, RegisterRequest, LoginRequest, UserResponse
|
||||
)
|
||||
from ...utils.redis_client import redis_client
|
||||
from ...utils.security import generate_verify_code, hash_password, verify_password, create_access_token
|
||||
from ...utils.sms import send_sms
|
||||
from ...utils.email import send_email
|
||||
from ...utils.logger import logger
|
||||
from ...middleware.rate_limit import rate_limit
|
||||
|
||||
router = APIRouter(prefix="/api/v1/auth", tags=["auth"])
|
||||
|
||||
def is_phone(account: str) -> bool:
|
||||
return re.match(r'^1[3-9]\d{9}$', account) is not None
|
||||
|
||||
def is_email(account: str) -> bool:
|
||||
return re.match(r'^[\w\.-]+@[\w\.-]+\.\w+$', account) is not None
|
||||
|
||||
@router.post("/send-code")
|
||||
@rate_limit(requests=1, period=60) # 同一账号每分钟1次
|
||||
async def send_verify_code(request: SendCodeRequest, db: DbDependency):
|
||||
"""发送验证码"""
|
||||
account = request.account
|
||||
account_type = request.type
|
||||
|
||||
# 验证账号格式
|
||||
if account_type == "phone" and not is_phone(account):
|
||||
raise HTTPException(status_code=400, detail="手机号格式错误")
|
||||
if account_type == "email" and not is_email(account):
|
||||
raise HTTPException(status_code=400, detail="邮箱格式错误")
|
||||
|
||||
# 检查是否已存在(注册时使用)
|
||||
# 这里只负责发送,不检查是否存在
|
||||
|
||||
# 生成验证码
|
||||
code = generate_verify_code()
|
||||
|
||||
# 存储到 Redis(5分钟有效)
|
||||
redis_key = f"verify:code:{account}"
|
||||
redis_client.set(redis_key, code, expire=300)
|
||||
|
||||
# 发送验证码
|
||||
success = False
|
||||
if account_type == "phone":
|
||||
success = send_sms(account, code)
|
||||
else:
|
||||
success = await send_email(account, code)
|
||||
|
||||
if not success:
|
||||
raise HTTPException(status_code=500, detail="验证码发送失败")
|
||||
|
||||
logger.info(f"验证码已发送: {account} -> {code}")
|
||||
return {"success": True, "message": "验证码已发送"}
|
||||
|
||||
@router.post("/register")
|
||||
@rate_limit(requests=5, period=60)
|
||||
async def register(request: RegisterRequest, db: DbDependency):
|
||||
"""注册(手机号/邮箱 + 验证码)"""
|
||||
account = request.account
|
||||
code = request.code
|
||||
password = request.password
|
||||
|
||||
# 验证验证码
|
||||
redis_key = f"verify:code:{account}"
|
||||
saved_code = redis_client.get(redis_key)
|
||||
if not saved_code or saved_code != code:
|
||||
raise HTTPException(status_code=400, detail="验证码错误或已过期")
|
||||
|
||||
# 检查用户是否已存在
|
||||
if is_phone(account):
|
||||
existing = db.query(User).filter(User.phone == account).first()
|
||||
if existing:
|
||||
raise HTTPException(status_code=400, detail="手机号已注册")
|
||||
user = User(phone=account, password_hash=hash_password(password))
|
||||
elif is_email(account):
|
||||
existing = db.query(User).filter(User.email == account).first()
|
||||
if existing:
|
||||
raise HTTPException(status_code=400, detail="邮箱已注册")
|
||||
user = User(email=account, password_hash=hash_password(password))
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail="账号格式错误")
|
||||
|
||||
db.add(user)
|
||||
db.commit()
|
||||
db.refresh(user)
|
||||
|
||||
# 删除已使用的验证码
|
||||
redis_client.delete(redis_key)
|
||||
|
||||
# 生成 token
|
||||
token = create_access_token({"sub": str(user.id)})
|
||||
|
||||
logger.info(f"新用户注册: {account}")
|
||||
return {
|
||||
"success": True,
|
||||
"token": token,
|
||||
"user": UserResponse.model_validate(user)
|
||||
}
|
||||
|
||||
@router.post("/login")
|
||||
@rate_limit(requests=10, period=60)
|
||||
async def login(request: LoginRequest, db: DbDependency):
|
||||
"""登录(支持验证码登录或密码登录)"""
|
||||
account = request.account
|
||||
password = request.password
|
||||
code = request.code
|
||||
|
||||
# 查找用户
|
||||
user = None
|
||||
if is_phone(account):
|
||||
user = db.query(User).filter(User.phone == account).first()
|
||||
elif is_email(account):
|
||||
user = db.query(User).filter(User.email == account).first()
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail="账号格式错误")
|
||||
|
||||
if not user:
|
||||
raise HTTPException(status_code=400, detail="用户不存在")
|
||||
|
||||
# 验证码登录
|
||||
if code:
|
||||
redis_key = f"verify:code:{account}"
|
||||
saved_code = redis_client.get(redis_key)
|
||||
if not saved_code or saved_code != code:
|
||||
raise HTTPException(status_code=400, detail="验证码错误或已过期")
|
||||
redis_client.delete(redis_key)
|
||||
|
||||
# 密码登录
|
||||
elif password:
|
||||
if not verify_password(password, user.password_hash):
|
||||
raise HTTPException(status_code=400, detail="密码错误")
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail="请提供验证码或密码")
|
||||
|
||||
# 更新最后登录时间
|
||||
from datetime import datetime
|
||||
user.last_login = datetime.now()
|
||||
db.commit()
|
||||
|
||||
token = create_access_token({"sub": str(user.id)})
|
||||
|
||||
logger.info(f"用户登录: {account}")
|
||||
return {
|
||||
"success": True,
|
||||
"token": token,
|
||||
"user": UserResponse.model_validate(user)
|
||||
}
|
||||
|
||||
@router.post("/wechat/login")
|
||||
async def wechat_login():
|
||||
"""微信公众号登录(预留)"""
|
||||
# TODO: 实现公众号 OAuth2 登录
|
||||
raise HTTPException(status_code=501, detail="功能开发中")
|
||||
79
backend/routers/v1/notes.py
Normal file
79
backend/routers/v1/notes.py
Normal file
@@ -0,0 +1,79 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
PerToolBox Server - 便签路由
|
||||
Copyright (C) 2024 Sea Network Technology Studio
|
||||
Author: Canglan <admin@sea-studio.top>
|
||||
License: AGPL v3
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
from typing import Optional, List
|
||||
from ...dependencies import CurrentUserDependency, DbDependency
|
||||
from ...models import Note
|
||||
from ...schemas import NoteCreate, NoteUpdate, NoteResponse
|
||||
from ...middleware.rate_limit import rate_limit
|
||||
|
||||
router = APIRouter(prefix="/api/v1/notes", tags=["notes"])
|
||||
|
||||
@router.get("/", response_model=List[NoteResponse])
|
||||
@rate_limit(requests=100, period=60)
|
||||
async def get_notes(
|
||||
current_user: CurrentUserDependency,
|
||||
db: DbDependency,
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(100, ge=1, le=500),
|
||||
archived: bool = False
|
||||
):
|
||||
return db.query(Note).filter(
|
||||
Note.user_id == current_user.id,
|
||||
Note.is_archived == archived
|
||||
).order_by(Note.created_at.desc()).offset(skip).limit(limit).all()
|
||||
|
||||
@router.post("/", response_model=NoteResponse, status_code=201)
|
||||
@rate_limit(requests=50, period=60)
|
||||
async def create_note(
|
||||
data: NoteCreate,
|
||||
current_user: CurrentUserDependency,
|
||||
db: DbDependency
|
||||
):
|
||||
note = Note(user_id=current_user.id, **data.model_dump())
|
||||
db.add(note)
|
||||
db.commit()
|
||||
db.refresh(note)
|
||||
return note
|
||||
|
||||
@router.put("/{note_id}", response_model=NoteResponse)
|
||||
@rate_limit(requests=50, period=60)
|
||||
async def update_note(
|
||||
note_id: int,
|
||||
data: NoteUpdate,
|
||||
current_user: CurrentUserDependency,
|
||||
db: DbDependency
|
||||
):
|
||||
note = db.query(Note).filter(Note.id == note_id, Note.user_id == current_user.id).first()
|
||||
if not note:
|
||||
raise HTTPException(status_code=404, detail="便签不存在")
|
||||
|
||||
update_data = data.model_dump(exclude_unset=True)
|
||||
for key, value in update_data.items():
|
||||
setattr(note, key, value)
|
||||
|
||||
db.commit()
|
||||
db.refresh(note)
|
||||
return note
|
||||
|
||||
@router.delete("/{note_id}")
|
||||
@rate_limit(requests=30, period=60)
|
||||
async def delete_note(
|
||||
note_id: int,
|
||||
current_user: CurrentUserDependency,
|
||||
db: DbDependency
|
||||
):
|
||||
note = db.query(Note).filter(Note.id == note_id, Note.user_id == current_user.id).first()
|
||||
if not note:
|
||||
raise HTTPException(status_code=404, detail="便签不存在")
|
||||
|
||||
db.delete(note)
|
||||
db.commit()
|
||||
return {"success": True, "message": "已删除"}
|
||||
81
backend/routers/v1/stats.py
Normal file
81
backend/routers/v1/stats.py
Normal file
@@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
PerToolBox Server - 热度统计路由
|
||||
Copyright (C) 2024 Sea Network Technology Studio
|
||||
Author: Canglan <admin@sea-studio.top>
|
||||
License: AGPL v3
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from fastapi import APIRouter
|
||||
from ...utils.redis_client import redis_client
|
||||
from ...models import ToolStatsTotal
|
||||
from ...dependencies import DbDependency
|
||||
from ...middleware.rate_limit import rate_limit
|
||||
|
||||
router = APIRouter(prefix="/api/v1", tags=["stats"])
|
||||
|
||||
# 预定义工具名称(对应前端页面)
|
||||
TOOL_NAMES = [
|
||||
"todos", "notes", "password", "qrcode",
|
||||
"crypto_hash", "crypto_base64", "crypto_url", "crypto_aes", "json"
|
||||
]
|
||||
|
||||
@router.post("/tool/usage")
|
||||
@rate_limit(requests=20, period=60)
|
||||
async def record_usage(tool_name: str, db: DbDependency):
|
||||
"""记录页面访问次数(热度)"""
|
||||
if tool_name not in TOOL_NAMES:
|
||||
raise HTTPException(status_code=400, detail="无效的工具名")
|
||||
|
||||
today = datetime.now().strftime("%Y-%m-%d")
|
||||
today_key = f"tool:stats:today:{tool_name}:{today}"
|
||||
total_key = f"tool:stats:total:{tool_name}"
|
||||
|
||||
# 增加今日计数(设置48小时过期)
|
||||
today_count = redis_client.incr(today_key)
|
||||
redis_client.expire(today_key, 48 * 3600)
|
||||
|
||||
# 增加总计数
|
||||
total_count = redis_client.incr(total_key)
|
||||
|
||||
# 异步更新 MySQL(可选,这里简单处理)
|
||||
# 实际可改为定时任务同步,此处为简化,直接更新
|
||||
stats = db.query(ToolStatsTotal).filter(ToolStatsTotal.tool_name == tool_name).first()
|
||||
if stats:
|
||||
stats.total_count = total_count
|
||||
else:
|
||||
stats = ToolStatsTotal(tool_name=tool_name, total_count=total_count)
|
||||
db.add(stats)
|
||||
db.commit()
|
||||
|
||||
return {"success": True}
|
||||
|
||||
@router.get("/tool/stats")
|
||||
@rate_limit(requests=100, period=60)
|
||||
async def get_stats(db: DbDependency):
|
||||
"""获取所有工具的今日/总访问次数"""
|
||||
today = datetime.now().strftime("%Y-%m-%d")
|
||||
result = {}
|
||||
|
||||
for tool_name in TOOL_NAMES:
|
||||
today_key = f"tool:stats:today:{tool_name}:{today}"
|
||||
total_key = f"tool:stats:total:{tool_name}"
|
||||
|
||||
today_count = redis_client.get(today_key)
|
||||
total_count = redis_client.get(total_key)
|
||||
|
||||
if total_count is None:
|
||||
# 从 MySQL 读取
|
||||
stats = db.query(ToolStatsTotal).filter(ToolStatsTotal.tool_name == tool_name).first()
|
||||
total_count = stats.total_count if stats else 0
|
||||
else:
|
||||
total_count = int(total_count)
|
||||
|
||||
result[tool_name] = {
|
||||
"today": int(today_count) if today_count else 0,
|
||||
"total": total_count
|
||||
}
|
||||
|
||||
return result
|
||||
84
backend/routers/v1/todos.py
Normal file
84
backend/routers/v1/todos.py
Normal file
@@ -0,0 +1,84 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
PerToolBox Server - 待办事项路由
|
||||
Copyright (C) 2024 Sea Network Technology Studio
|
||||
Author: Canglan <admin@sea-studio.top>
|
||||
License: AGPL v3
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
from typing import Optional, List
|
||||
from ...dependencies import CurrentUserDependency, DbDependency
|
||||
from ...models import Todo
|
||||
from ...schemas import TodoCreate, TodoUpdate, TodoResponse
|
||||
from ...middleware.rate_limit import rate_limit
|
||||
|
||||
router = APIRouter(prefix="/api/v1/todos", tags=["todos"])
|
||||
|
||||
@router.get("/", response_model=List[TodoResponse])
|
||||
@rate_limit(requests=100, period=60)
|
||||
async def get_todos(
|
||||
current_user: CurrentUserDependency,
|
||||
db: DbDependency,
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(100, ge=1, le=500),
|
||||
completed: Optional[bool] = None,
|
||||
category: Optional[str] = None
|
||||
):
|
||||
query = db.query(Todo).filter(Todo.user_id == current_user.id)
|
||||
|
||||
if completed is not None:
|
||||
query = query.filter(Todo.completed == completed)
|
||||
if category:
|
||||
query = query.filter(Todo.category == category)
|
||||
|
||||
return query.order_by(Todo.priority.desc(), Todo.created_at.desc()).offset(skip).limit(limit).all()
|
||||
|
||||
@router.post("/", response_model=TodoResponse, status_code=201)
|
||||
@rate_limit(requests=50, period=60)
|
||||
async def create_todo(
|
||||
data: TodoCreate,
|
||||
current_user: CurrentUserDependency,
|
||||
db: DbDependency
|
||||
):
|
||||
todo = Todo(user_id=current_user.id, **data.model_dump())
|
||||
db.add(todo)
|
||||
db.commit()
|
||||
db.refresh(todo)
|
||||
return todo
|
||||
|
||||
@router.put("/{todo_id}", response_model=TodoResponse)
|
||||
@rate_limit(requests=50, period=60)
|
||||
async def update_todo(
|
||||
todo_id: int,
|
||||
data: TodoUpdate,
|
||||
current_user: CurrentUserDependency,
|
||||
db: DbDependency
|
||||
):
|
||||
todo = db.query(Todo).filter(Todo.id == todo_id, Todo.user_id == current_user.id).first()
|
||||
if not todo:
|
||||
raise HTTPException(status_code=404, detail="待办事项不存在")
|
||||
|
||||
update_data = data.model_dump(exclude_unset=True)
|
||||
for key, value in update_data.items():
|
||||
setattr(todo, key, value)
|
||||
|
||||
db.commit()
|
||||
db.refresh(todo)
|
||||
return todo
|
||||
|
||||
@router.delete("/{todo_id}")
|
||||
@rate_limit(requests=30, period=60)
|
||||
async def delete_todo(
|
||||
todo_id: int,
|
||||
current_user: CurrentUserDependency,
|
||||
db: DbDependency
|
||||
):
|
||||
todo = db.query(Todo).filter(Todo.id == todo_id, Todo.user_id == current_user.id).first()
|
||||
if not todo:
|
||||
raise HTTPException(status_code=404, detail="待办事项不存在")
|
||||
|
||||
db.delete(todo)
|
||||
db.commit()
|
||||
return {"success": True, "message": "已删除"}
|
||||
183
backend/routers/v1/tools.py
Normal file
183
backend/routers/v1/tools.py
Normal file
@@ -0,0 +1,183 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
PerToolBox Server - 工具路由(密码、二维码、加密、JSON)
|
||||
Copyright (C) 2024 Sea Network Technology Studio
|
||||
Author: Canglan <admin@sea-studio.top>
|
||||
License: AGPL v3
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import base64
|
||||
import urllib.parse
|
||||
import json
|
||||
import qrcode
|
||||
from io import BytesIO
|
||||
import base64 as b64
|
||||
from Crypto.Cipher import AES
|
||||
from Crypto.Util.Padding import pad, unpad
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from ...schemas import (
|
||||
HashRequest, Base64Request, URLRequest, AESRequest,
|
||||
JSONValidateRequest, JSONValidateResponse
|
||||
)
|
||||
from ...middleware.rate_limit import rate_limit
|
||||
|
||||
router = APIRouter(prefix="/api/v1", tags=["tools"])
|
||||
|
||||
# ========== 密码生成 ==========
|
||||
@router.get("/password/generate")
|
||||
@rate_limit(requests=50, period=60)
|
||||
async def generate_password(
|
||||
length: int = 12,
|
||||
upper: bool = True,
|
||||
lower: bool = True,
|
||||
digits: bool = True,
|
||||
symbols: bool = True,
|
||||
count: int = 1
|
||||
):
|
||||
import random
|
||||
import string
|
||||
|
||||
chars = ""
|
||||
if upper:
|
||||
chars += string.ascii_uppercase
|
||||
if lower:
|
||||
chars += string.ascii_lowercase
|
||||
if digits:
|
||||
chars += string.digits
|
||||
if symbols:
|
||||
chars += "!@#$%^&*"
|
||||
|
||||
if not chars:
|
||||
chars = string.ascii_letters + string.digits
|
||||
|
||||
passwords = [''.join(random.choice(chars) for _ in range(length)) for _ in range(count)]
|
||||
return {"passwords": passwords if count > 1 else passwords[0]}
|
||||
|
||||
# ========== 二维码 ==========
|
||||
@router.post("/qrcode/generate")
|
||||
@rate_limit(requests=30, period=60)
|
||||
async def generate_qrcode(content: str, size: int = 10):
|
||||
if not content:
|
||||
raise HTTPException(status_code=400, detail="内容不能为空")
|
||||
|
||||
qr = qrcode.QRCode(box_size=size, border=2)
|
||||
qr.add_data(content)
|
||||
qr.make(fit=True)
|
||||
img = qr.make_image(fill_color="black", back_color="white")
|
||||
|
||||
buffered = BytesIO()
|
||||
img.save(buffered, format="PNG")
|
||||
img_base64 = b64.b64encode(buffered.getvalue()).decode()
|
||||
|
||||
return {"qr_code": f"data:image/png;base64,{img_base64}"}
|
||||
|
||||
# ========== 哈希 ==========
|
||||
@router.post("/crypto/hash")
|
||||
@rate_limit(requests=100, period=60)
|
||||
async def compute_hash(request: HashRequest):
|
||||
text = request.text.encode('utf-8')
|
||||
algo = request.algorithm.lower()
|
||||
|
||||
if algo == "md5":
|
||||
result = hashlib.md5(text).hexdigest()
|
||||
elif algo == "sha1":
|
||||
result = hashlib.sha1(text).hexdigest()
|
||||
elif algo == "sha256":
|
||||
result = hashlib.sha256(text).hexdigest()
|
||||
elif algo == "sha512":
|
||||
result = hashlib.sha512(text).hexdigest()
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail="不支持的算法")
|
||||
|
||||
return {"algorithm": algo, "result": result}
|
||||
|
||||
# ========== Base64 ==========
|
||||
@router.post("/crypto/base64")
|
||||
@rate_limit(requests=100, period=60)
|
||||
async def base64_process(request: Base64Request):
|
||||
if request.action == "encode":
|
||||
result = base64.b64encode(request.text.encode('utf-8')).decode('utf-8')
|
||||
elif request.action == "decode":
|
||||
try:
|
||||
result = base64.b64decode(request.text).decode('utf-8')
|
||||
except Exception:
|
||||
raise HTTPException(status_code=400, detail="Base64 解码失败")
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail="无效的 action")
|
||||
|
||||
return {"action": request.action, "result": result}
|
||||
|
||||
# ========== URL 编解码 ==========
|
||||
@router.post("/crypto/url")
|
||||
@rate_limit(requests=100, period=60)
|
||||
async def url_process(request: URLRequest):
|
||||
if request.action == "encode":
|
||||
result = urllib.parse.quote(request.text, safe='')
|
||||
elif request.action == "decode":
|
||||
result = urllib.parse.unquote(request.text)
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail="无效的 action")
|
||||
|
||||
return {"action": request.action, "result": result}
|
||||
|
||||
# ========== AES 加解密 ==========
|
||||
@router.post("/crypto/aes")
|
||||
@rate_limit(requests=50, period=60)
|
||||
async def aes_process(request: AESRequest):
|
||||
try:
|
||||
key = request.key.encode('utf-8')
|
||||
mode_map = {"ECB": AES.MODE_ECB, "CBC": AES.MODE_CBC, "GCM": AES.MODE_GCM}
|
||||
mode = mode_map.get(request.mode)
|
||||
|
||||
if not mode:
|
||||
raise HTTPException(status_code=400, detail="不支持的 AES 模式")
|
||||
|
||||
# 密钥长度处理
|
||||
if len(key) not in [16, 24, 32]:
|
||||
raise HTTPException(status_code=400, detail="密钥长度必须为 16/24/32 字节")
|
||||
|
||||
if request.action == "encrypt":
|
||||
cipher = AES.new(key, mode, iv=request.iv.encode('utf-8') if request.iv else None)
|
||||
if request.mode == "GCM":
|
||||
ciphertext = cipher.encrypt(request.text.encode('utf-8'))
|
||||
result = b64.b64encode(ciphertext).decode('utf-8')
|
||||
else:
|
||||
padded = pad(request.text.encode('utf-8'), AES.block_size)
|
||||
ciphertext = cipher.encrypt(padded)
|
||||
result = b64.b64encode(ciphertext).decode('utf-8')
|
||||
elif request.action == "decrypt":
|
||||
ciphertext = b64.b64decode(request.text)
|
||||
cipher = AES.new(key, mode, iv=request.iv.encode('utf-8') if request.iv else None)
|
||||
if request.mode == "GCM":
|
||||
plaintext = cipher.decrypt(ciphertext).decode('utf-8')
|
||||
result = plaintext
|
||||
else:
|
||||
plaintext_padded = cipher.decrypt(ciphertext)
|
||||
result = unpad(plaintext_padded, AES.block_size).decode('utf-8')
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail="无效的 action")
|
||||
|
||||
return {"mode": request.mode, "action": request.action, "result": result}
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=f"AES 操作失败: {str(e)}")
|
||||
|
||||
# ========== JSON 校验 ==========
|
||||
@router.post("/json/validate", response_model=JSONValidateResponse)
|
||||
@rate_limit(requests=100, period=60)
|
||||
async def validate_json(request: JSONValidateRequest):
|
||||
try:
|
||||
parsed = json.loads(request.json_string)
|
||||
formatted = json.dumps(parsed, indent=2, ensure_ascii=False)
|
||||
return JSONValidateResponse(valid=True, formatted=formatted)
|
||||
except json.JSONDecodeError as e:
|
||||
return JSONValidateResponse(
|
||||
valid=False,
|
||||
error={
|
||||
"line": e.lineno,
|
||||
"column": e.colno,
|
||||
"message": e.msg
|
||||
}
|
||||
)
|
||||
57
backend/routers/v1/user.py
Normal file
57
backend/routers/v1/user.py
Normal file
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
PerToolBox Server - 用户路由
|
||||
Copyright (C) 2024 Sea Network Technology Studio
|
||||
Author: Canglan <admin@sea-studio.top>
|
||||
License: AGPL v3
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from ...dependencies import CurrentUserDependency, DbDependency
|
||||
from ...models import User
|
||||
from ...schemas import UserResponse, UserUpdateRequest
|
||||
from ...utils.security import hash_password, verify_password
|
||||
from ...utils.logger import logger
|
||||
|
||||
router = APIRouter(prefix="/api/v1/user", tags=["user"])
|
||||
|
||||
@router.get("/profile", response_model=UserResponse)
|
||||
async def get_profile(current_user: CurrentUserDependency):
|
||||
"""获取当前用户信息"""
|
||||
return current_user
|
||||
|
||||
@router.put("/profile", response_model=UserResponse)
|
||||
async def update_profile(
|
||||
request: UserUpdateRequest,
|
||||
current_user: CurrentUserDependency,
|
||||
db: DbDependency
|
||||
):
|
||||
"""更新用户信息"""
|
||||
if request.username:
|
||||
current_user.username = request.username
|
||||
if request.avatar:
|
||||
current_user.avatar = request.avatar
|
||||
|
||||
db.commit()
|
||||
db.refresh(current_user)
|
||||
|
||||
logger.info(f"用户信息更新: {current_user.id}")
|
||||
return current_user
|
||||
|
||||
@router.post("/change-password")
|
||||
async def change_password(
|
||||
old_password: str,
|
||||
new_password: str,
|
||||
current_user: CurrentUserDependency,
|
||||
db: DbDependency
|
||||
):
|
||||
"""修改密码"""
|
||||
if not verify_password(old_password, current_user.password_hash):
|
||||
raise HTTPException(status_code=400, detail="原密码错误")
|
||||
|
||||
current_user.password_hash = hash_password(new_password)
|
||||
db.commit()
|
||||
|
||||
logger.info(f"密码修改: {current_user.id}")
|
||||
return {"success": True, "message": "密码已修改"}
|
||||
Reference in New Issue
Block a user