Initial commit - DesignerCEP Project with Caddy deployment

This commit is contained in:
zuowei1216
2025-12-19 21:27:17 +08:00
commit 8ea58fe480
170 changed files with 47469 additions and 0 deletions

107
Server/app/api/v1/admin.py Normal file
View File

@@ -0,0 +1,107 @@
import os
import shutil
from fastapi import APIRouter, Depends, UploadFile, File, HTTPException, status, Form
from sqlalchemy.orm import Session
from app.db import get_db
from app.models.group import PluginGroup as DBPluginGroup
from app.models.user import User
from app.schemas.group import PluginGroupCreate, PluginGroupUpdate, PluginGroup
from app.schemas.admin import UserInfo
from app.core.config import settings
from typing import List
router = APIRouter()
# Hardcoded admin token for simplicity as per requirements
ADMIN_TOKEN = "admin-secret-token"
def verify_admin(token: str = Form(...)):
if token != ADMIN_TOKEN:
raise HTTPException(status_code=403, detail="Admin permission required")
def get_admin_dep(x_admin_token: str = None):
# Alternative using header
if x_admin_token != ADMIN_TOKEN:
raise HTTPException(status_code=403, detail="Admin permission required")
# Ensure archives directory exists
ARCHIVES_DIR = "archives"
os.makedirs(ARCHIVES_DIR, exist_ok=True)
@router.post("/upload_version")
async def upload_version(
file: UploadFile = File(...),
# token: str = Form(...), # Simple auth
db: Session = Depends(get_db)
):
# if token != ADMIN_TOKEN:
# raise HTTPException(status_code=403, detail="Invalid admin token")
file_location = os.path.join(ARCHIVES_DIR, file.filename)
with open(file_location, "wb+") as file_object:
shutil.copyfileobj(file.file, file_object)
return {"code": 200, "message": f"File '{file.filename}' uploaded successfully", "filename": file.filename}
@router.get("/archives")
async def list_archives():
if not os.path.exists(ARCHIVES_DIR):
return []
files = os.listdir(ARCHIVES_DIR)
# Sort by name (which usually includes timestamp) desc
files.sort(reverse=True)
return files
@router.post("/groups", response_model=PluginGroup)
async def create_group(group: PluginGroupCreate, db: Session = Depends(get_db)):
db_group = DBPluginGroup(**group.model_dump())
db.add(db_group)
db.commit()
db.refresh(db_group)
return db_group
@router.get("/groups", response_model=List[PluginGroup])
async def list_groups(db: Session = Depends(get_db)):
return db.query(DBPluginGroup).all()
@router.put("/groups/{group_id}", response_model=PluginGroup)
async def update_group(group_id: int, group_update: PluginGroupUpdate, db: Session = Depends(get_db)):
db_group = db.query(DBPluginGroup).filter(DBPluginGroup.id == group_id).first()
if not db_group:
raise HTTPException(status_code=404, detail="Group not found")
update_data = group_update.model_dump(exclude_unset=True)
for key, value in update_data.items():
setattr(db_group, key, value)
db.commit()
db.refresh(db_group)
return db_group
@router.get("/users", response_model=List[UserInfo])
async def list_users(db: Session = Depends(get_db)):
return db.query(User).all()
@router.put("/users/{user_id}/group")
async def update_user_group(user_id: int, group_id: int, db: Session = Depends(get_db)):
user = db.query(User).filter(User.id == user_id).first()
if not user:
raise HTTPException(status_code=404, detail="User not found")
group = db.query(DBPluginGroup).filter(DBPluginGroup.id == group_id).first()
if not group:
raise HTTPException(status_code=404, detail="Group not found")
user.group_id = group_id
db.commit()
return {"code": 200, "message": "User group updated"}
@router.put("/users/{user_id}/permissions")
async def update_user_permissions(user_id: int, permissions: str = Form(...), db: Session = Depends(get_db)):
user = db.query(User).filter(User.id == user_id).first()
if not user:
raise HTTPException(status_code=404, detail="User not found")
user.permissions = permissions
db.commit()
return {"code": 200, "message": "User permissions updated"}

View File

@@ -0,0 +1,83 @@
"""
用户行为分析 API
记录和分析用户操作
"""
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from typing import Optional, Any
from datetime import datetime
router = APIRouter()
# 简单的内存存储(生产环境应该用数据库)
action_logs = []
class ActionLog(BaseModel):
username: str
device_id: str
action: str
details: Optional[Any] = None
timestamp: int
session_id: str
class ActionLogResponse(BaseModel):
success: bool
message: str
@router.post("/log", response_model=ActionLogResponse)
async def log_action(log: ActionLog):
"""
记录用户行为
"""
try:
# 添加服务器时间
log_entry = {
**log.dict(),
"server_time": datetime.now().isoformat(),
"ip": "unknown" # 可以从请求中获取
}
action_logs.append(log_entry)
# 只保留最近 10000 条
if len(action_logs) > 10000:
action_logs.pop(0)
# 可以在这里添加异常检测逻辑
# 例如:检测同一用户短时间内的大量操作
return ActionLogResponse(success=True, message="已记录")
except Exception as e:
return ActionLogResponse(success=False, message=str(e))
@router.get("/stats/{username}")
async def get_user_stats(username: str):
"""
获取用户统计信息
"""
user_logs = [log for log in action_logs if log.get("username") == username]
# 统计各操作类型的次数
action_counts = {}
for log in user_logs:
action = log.get("action", "unknown")
action_counts[action] = action_counts.get(action, 0) + 1
return {
"username": username,
"total_actions": len(user_logs),
"action_counts": action_counts,
"recent_actions": user_logs[-10:] # 最近 10 条
}
@router.get("/recent")
async def get_recent_logs(limit: int = 100):
"""
获取最近的操作日志(管理员用)
"""
return {
"total": len(action_logs),
"logs": action_logs[-limit:]
}

99
Server/app/api/v1/auth.py Normal file
View File

@@ -0,0 +1,99 @@
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from app.schemas.auth import (
UserLogin, UserRegister, Token, UserLogout, UserHeartbeat,
VerifyRequest, VerifyResponse, VerifyEmailRequest,
ForgotPasswordRequest, ResetPasswordRequest, SendVerificationCodeRequest
)
from app.services.auth_service import auth_service
from app.db import get_db
from app.models.session import UserSession
from app.models.user import User
from app.core.security import get_current_user
from datetime import datetime, timezone
router = APIRouter()
@router.post("/send-verification-code")
async def send_verification_code(body: SendVerificationCodeRequest, db: Session = Depends(get_db)):
# 发送注册验证码
return auth_service.send_verification_code(db, body.email)
@router.post("/login", response_model=Token)
async def login(login_data: UserLogin, db: Session = Depends(get_db)):
# 登录接口:校验用户密码,返回访问令牌
return auth_service.login(db, login_data)
@router.post("/register", response_model=Token)
async def register(register_data: UserRegister, db: Session = Depends(get_db)):
# 注册接口:创建新用户,返回访问令牌
return auth_service.register(db, register_data)
@router.post("/verify-email")
async def verify_email(body: VerifyEmailRequest, db: Session = Depends(get_db)):
return auth_service.verify_email(db, body.username, body.code)
@router.post("/forgot-password")
async def forgot_password(body: ForgotPasswordRequest, db: Session = Depends(get_db)):
return auth_service.forgot_password(db, body.email)
@router.post("/reset-password")
async def reset_password(body: ResetPasswordRequest, db: Session = Depends(get_db)):
if body.new_password != body.confirm_password:
raise HTTPException(status_code=400, detail="两次输入的密码不一致")
# 传入 email 参数
return auth_service.reset_password(db, body.token, body.new_password, body.email)
@router.post("/logout")
async def logout(body: UserLogout, db: Session = Depends(get_db)):
# 登出接口:将指定设备会话置为非活跃
return auth_service.logout(db, body.username, body.device_id)
@router.get("/online-time/{username}")
async def get_online_time(username: str, db: Session = Depends(get_db)):
# 在线时长统计:累计历史会话的时长(秒),以及当前活跃会话的实时时长(秒)
user = db.query(User).filter(User.username == username).first()
if not user:
return {"username": username, "total_seconds": 0, "active_seconds": 0}
# 历史累计(已登出的会话)
total = db.query(UserSession).filter(
UserSession.user_id == user.id,
UserSession.active == False,
UserSession.duration_seconds != None
).with_entities(UserSession.duration_seconds).all()
total_seconds = sum([d[0] for d in total]) if total else 0
# 当前活跃会话实时时长
now = datetime.now(timezone.utc)
active = db.query(UserSession).filter(
UserSession.user_id == user.id,
UserSession.active == True,
UserSession.login_at != None
).first()
if active:
login_at = active.login_at
if login_at and login_at.tzinfo is None:
login_at = login_at.replace(tzinfo=timezone.utc)
last_seen = active.last_seen_at or login_at
if last_seen and last_seen.tzinfo is None:
last_seen = last_seen.replace(tzinfo=timezone.utc)
# 判定是否在线:如果 last_seen 在最近 2 分钟内,则认为在线,用 now 计算实时时长
# 否则认为已断开(异常退出),用 last_seen 计算截止时长
# 阈值设为 120 秒(假设前端心跳间隔为 60 秒)
is_online = (now - last_seen).total_seconds() < 120
if is_online:
end_time = now
else:
end_time = last_seen
active_seconds = int(max(0, (end_time - login_at).total_seconds())) if login_at else 0
else:
active_seconds = 0
return {"username": username, "total_seconds": total_seconds, "active_seconds": active_seconds}
@router.post("/heartbeat")
async def heartbeat(body: UserHeartbeat, db: Session = Depends(get_db)):
# 心跳接口:更新会话的最近在线时间
return auth_service.heartbeat(db, body.username, body.device_id)

View File

@@ -0,0 +1,36 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from app.schemas.client import CheckUpdateRequest, CheckUpdateResponse, LoginResponse
from app.schemas.auth import UserLogin
from app.services.group_service import group_service
from app.services.auth_service import auth_service
from app.db import get_db
router = APIRouter()
@router.post("/check_update", response_model=CheckUpdateResponse)
async def check_update(request: CheckUpdateRequest, db: Session = Depends(get_db)):
try:
data = group_service.check_update(db, request.username)
return CheckUpdateResponse(code=200, data=data, message="success")
except HTTPException as e:
# Wrap HTTPException to match response format if needed, or let global handler handle it.
# Requirements imply specific format.
# But usually 4xx/5xx are handled by exception handlers.
# If I want to return 200 with code=404 in body (anti-pattern but possible), I should do it here.
# The example shows code=200.
# Let's assume standard HTTP status codes for errors, but if successful, return wrapped data.
raise e
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/login", response_model=LoginResponse)
async def login(login_data: UserLogin, db: Session = Depends(get_db)):
# Re-use UserLogin schema as it matches {username, password} + device_id (optional in spec but present in schema)
# Spec says {username, password}, UserLogin has device_id.
# If device_id is missing in request, validation fails.
# Spec example doesn't show device_id in request, but "Backend Development Guidelines" 4.5 says "Must provide stable device_id".
# So I will assume the client sends device_id.
data = auth_service.client_login(db, login_data)
return LoginResponse(code=200, data=data, message="success")

View File

@@ -0,0 +1,120 @@
"""
服务器端计算 Demo - 简单数学计算
演示:前端获取图层名称 → 后端计算数学表达式 → 返回结果
"""
from fastapi import APIRouter, Header, HTTPException
from pydantic import BaseModel
import re
import logging
from datetime import datetime
from typing import Optional
from app.core.api_keys import validate_api_key, get_key_info
# 配置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [%(levelname)s] %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
logger = logging.getLogger(__name__)
router = APIRouter()
class CalculateRequest(BaseModel):
"""计算请求"""
expression: str # 数学表达式,如 "87-98"
class CalculateResult(BaseModel):
"""计算结果"""
success: bool
expression: str
result: float = None
message: str
@router.post("/calculate", response_model=CalculateResult)
async def calculate_expression(
request: CalculateRequest,
x_api_key: Optional[str] = Header(None) # 可选的 API Key 验证
):
"""
🔒 服务器端数学计算(核心算法)
客户端只能拿到计算结果,看不到算法
示例:前端发送 "87-98" → 后端计算 → 返回 -11
"""
# ==================== 📝 日志:打印请求 ====================
logger.info("="*60)
logger.info("📥 收到计算请求")
logger.info(f" 时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
logger.info(f" 表达式: {request.expression}")
logger.info(f" API Key: {x_api_key if x_api_key else '未提供'}")
logger.info("="*60)
# ==================== 🔐 API Key 验证 ====================
if not validate_api_key(x_api_key):
logger.warning(f"❌ API Key 验证失败: {x_api_key}")
raise HTTPException(status_code=403, detail="无效的 API Key")
# 获取 Key 信息
key_info = get_key_info(x_api_key)
logger.info(f"✅ API Key 验证通过 | 名称: {key_info['name']} | 权限: {key_info['permissions']}")
try:
# 🔒 核心算法在这里(客户端看不到)
# 可以是复杂的数学模型、AI 推理等
expression = request.expression.strip()
# ==================== 🛡️ 安全检查 ====================
logger.info(f"🛡️ 安全检查: 验证表达式格式...")
# 只允许数字和基本运算符
if not re.match(r'^[\d\s\+\-\*\/\(\)\.]+$', expression):
logger.warning(f"❌ 表达式包含非法字符: {expression}")
return CalculateResult(
success=False,
expression=expression,
message="只支持基本数学运算(+、-、*、/"
)
logger.info("✅ 表达式格式验证通过")
# ==================== 🔒 核心算法执行 ====================
logger.info("🔒 开始执行核心算法...")
# 这里可以放你的核心算法
# 示例:简单计算
result = eval(expression)
logger.info(f"✅ 计算完成: {expression} = {result}")
# ==================== 📤 日志:打印输出 ====================
response = CalculateResult(
success=True,
expression=expression,
result=float(result),
message=f"计算成功: {expression} = {result}"
)
logger.info("="*60)
logger.info("📤 返回计算结果")
logger.info(f" 成功: {response.success}")
logger.info(f" 表达式: {response.expression}")
logger.info(f" 结果: {response.result}")
logger.info(f" 消息: {response.message}")
logger.info("="*60)
return response
except Exception as e:
logger.error(f"❌ 计算失败: {str(e)}")
logger.error("="*60)
return CalculateResult(
success=False,
expression=request.expression,
message=f"计算失败: {str(e)}"
)

View File

@@ -0,0 +1,140 @@
"""
服务器端 JSX 执行器
关键业务逻辑在服务器执行,前端只能通过 API 调用
"""
from fastapi import APIRouter, HTTPException, Depends
from pydantic import BaseModel
from typing import Optional, Dict, Any
from app.core.security import get_current_user
from app.db import get_db_session
import json
router = APIRouter()
# JSX 脚本模板库(服务器端存储)
JSX_TEMPLATES = {
"create_layer": """
(function(layerName) {
try {
if (app.documents.length === 0) {
return JSON.stringify({ error: '没有打开的文档' });
}
var doc = app.activeDocument;
var layer = doc.artLayers.add();
layer.name = layerName;
return JSON.stringify({
success: true,
layerName: layerName
});
} catch (e) {
return JSON.stringify({ error: e.toString() });
}
})('{layerName}')
""",
"create_layer_with_style": """
(function(layerName, opacity, color) {
try {
if (app.documents.length === 0) {
return JSON.stringify({ error: '没有打开的文档' });
}
var doc = app.activeDocument;
var layer = doc.artLayers.add();
layer.name = layerName;
layer.opacity = opacity;
// 这里是你的核心算法
// 客户端无法看到具体实现
return JSON.stringify({
success: true,
layerName: layerName,
applied: true
});
} catch (e) {
return JSON.stringify({ error: e.toString() });
}
})('{layerName}', {opacity}, '{color}')
""",
# 更多核心功能...
}
class JSXExecuteRequest(BaseModel):
template_name: str # 模板名称(不是完整脚本)
params: Dict[str, Any] # 参数
device_id: str
class JSXExecuteResponse(BaseModel):
success: bool
jsx_code: str # 返回要执行的 JSX 代码
message: Optional[str] = None
@router.post("/execute", response_model=JSXExecuteResponse)
async def execute_jsx(
request: JSXExecuteRequest,
current_user: dict = Depends(get_current_user)
):
"""
服务器端生成 JSX 代码
客户端只能通过 API 获取,无法直接看到核心逻辑
"""
try:
username = current_user.get("username")
# 1. 验证用户和设备
# ... (从数据库检查用户是否有权限、设备是否绑定)
# 2. 检查模板是否存在
if request.template_name not in JSX_TEMPLATES:
raise HTTPException(status_code=404, detail="模板不存在")
# 3. 验证用户权限(某些高级功能需要付费)
# ... (从数据库检查用户等级)
# 4. 获取模板并填充参数
template = JSX_TEMPLATES[request.template_name]
# 参数安全过滤(防止注入)
safe_params = {
key: str(value).replace("'", "\\'").replace('"', '\\"')
for key, value in request.params.items()
}
# 填充参数
jsx_code = template.format(**safe_params)
# 5. 记录操作日志
# ... (记录谁在什么时候执行了什么操作)
return JSXExecuteResponse(
success=True,
jsx_code=jsx_code,
message="代码已生成"
)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/templates")
async def list_templates(current_user: dict = Depends(get_current_user)):
"""
列出可用的 JSX 模板(不返回具体代码)
"""
return {
"templates": [
{
"name": "create_layer",
"description": "创建图层",
"params": ["layerName"]
},
{
"name": "create_layer_with_style",
"description": "创建带样式的图层(高级)",
"params": ["layerName", "opacity", "color"],
"premium": True # 需要付费
}
]
}