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

View File

@@ -0,0 +1,80 @@
"""
API Key 管理
用于验证客户端请求
"""
from datetime import datetime
from typing import Optional, Dict
class APIKeyManager:
"""API Key 管理器"""
# 🔐 有效的 API Keys
# 生产环境建议从环境变量或数据库读取
VALID_KEYS: Dict[str, dict] = {
"demo_key_123": {
"name": "测试密钥",
"created": "2024-12-16",
"permissions": ["calculate"],
"rate_limit": 100 # 每小时最多 100 次请求
},
"prod_key_xyz789abc": {
"name": "生产密钥",
"created": "2024-12-16",
"permissions": ["calculate", "admin"],
"rate_limit": 1000
}
}
@classmethod
def validate_key(cls, api_key: Optional[str]) -> bool:
"""验证 API Key 是否有效"""
if not api_key:
return False
return api_key in cls.VALID_KEYS
@classmethod
def get_key_info(cls, api_key: str) -> Optional[dict]:
"""获取 API Key 信息"""
return cls.VALID_KEYS.get(api_key)
@classmethod
def check_permission(cls, api_key: str, permission: str) -> bool:
"""检查 API Key 是否有指定权限"""
key_info = cls.get_key_info(api_key)
if not key_info:
return False
return permission in key_info.get("permissions", [])
@classmethod
def add_key(cls, api_key: str, name: str, permissions: list = None):
"""添加新的 API Key"""
cls.VALID_KEYS[api_key] = {
"name": name,
"created": datetime.now().strftime("%Y-%m-%d"),
"permissions": permissions or ["calculate"],
"rate_limit": 100
}
@classmethod
def remove_key(cls, api_key: str):
"""删除 API Key"""
if api_key in cls.VALID_KEYS:
del cls.VALID_KEYS[api_key]
@classmethod
def list_keys(cls) -> Dict[str, dict]:
"""列出所有 API Keys"""
return cls.VALID_KEYS.copy()
# 快捷函数
def validate_api_key(api_key: Optional[str]) -> bool:
"""验证 API Key"""
return APIKeyManager.validate_key(api_key)
def get_key_info(api_key: str) -> Optional[dict]:
"""获取 API Key 信息"""
return APIKeyManager.get_key_info(api_key)

24
Server/app/core/config.py Normal file
View File

@@ -0,0 +1,24 @@
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
ENV: str = "development"
PROJECT_NAME: str = "DesignerCEP Backend"
API_V1_STR: str = "/api/v1"
DATABASE_URL: str = "sqlite:///./designercep.db"
SECRET_KEY: str = "change-me"
ACCESS_TOKEN_EXPIRE_MINUTES: int = 10080
ALLOWED_ORIGINS: str = "*"
ADMIN_TOKEN: str = "admin-token"
# Email Configuration
SMTP_HOST: str = "smtp.gmail.com"
SMTP_PORT: int = 587
SMTP_USER: str = "ly1104803132@gmail.com"
SMTP_PASSWORD: str = "wsfrpnmkojpsqdkk"
EMAILS_FROM_EMAIL: str = "ly1104803132@gmail.com"
EMAILS_FROM_NAME: str = "Designer"
model_config = SettingsConfigDict(env_file=".env", extra="ignore")
settings = Settings()

115
Server/app/core/security.py Normal file
View File

@@ -0,0 +1,115 @@
from datetime import datetime, timedelta
from typing import Optional
import jwt
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from passlib.context import CryptContext
from sqlalchemy.orm import Session
from app.core.config import settings
from app.db import get_db
from app.models.session import UserSession
# 密码哈希配置(使用 bcrypt
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
oauth2_scheme = OAuth2PasswordBearer(tokenUrl=f"{settings.API_V1_STR}/auth/login")
def verify_password(plain_password: str, hashed_password: str) -> bool:
# 验证明文密码与哈希是否匹配
return pwd_context.verify(plain_password, hashed_password)
def get_password_hash(password: str) -> str:
# 对密码进行哈希
return pwd_context.hash(password)
def create_access_token(subject: str, device_id: str, expires_delta: Optional[timedelta] = None) -> str:
# 创建 JWT 访问令牌,默认过期时间从配置读取
expire = datetime.utcnow() + (expires_delta or timedelta(minutes=getattr(settings, "ACCESS_TOKEN_EXPIRE_MINUTES", 60)))
# 将 device_id 写入 Token Payload
to_encode = {"sub": subject, "device_id": device_id, "exp": expire}
return jwt.encode(to_encode, settings.SECRET_KEY, algorithm="HS256")
def get_current_user(token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)) -> str:
"""
验证 JWT Token 并返回当前用户名 (sub)
增加 Session 强校验:检查数据库中 Session 是否活跃
"""
print("="*60)
print("[get_current_user] 开始验证 Token")
print(f" - Token (前30字符): {token[:30]}...")
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="无效的认证凭据",
headers={"WWW-Authenticate": "Bearer"},
)
# 1. 解析 Token
try:
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=["HS256"])
username: str = payload.get("sub")
device_id: str = payload.get("device_id")
print(f"[get_current_user] ✓ Token 解析成功")
print(f" - username: {username}")
print(f" - device_id: {device_id}")
print(f" - exp: {payload.get('exp')}")
if username is None:
print("[get_current_user] ✗ username 为空")
raise credentials_exception
except jwt.ExpiredSignatureError:
print("[get_current_user] ✗ Token 已过期")
raise credentials_exception
except jwt.InvalidTokenError as e:
print(f"[get_current_user] ✗ Token 无效: {e}")
raise credentials_exception
except jwt.PyJWTError as e:
print(f"[get_current_user] ✗ Token 解析失败: {e}")
raise credentials_exception
# 2. Session 强校验 (如果有 device_id)
# 如果 Token 是旧版本没有 device_id可以选择放行或拒绝。为了安全建议逐步拒绝。
# 这里我们假设所有新 Token 都有 device_id
if device_id:
print(f"[get_current_user] 开始 Session 强校验")
# 查询 User ID
from app.models.user import User
user = db.query(User).filter(User.username == username).first()
if not user:
print(f"[get_current_user] ✗ 用户不存在: {username}")
raise credentials_exception
print(f"[get_current_user] ✓ 用户存在: user_id={user.id}")
# 查询活跃 Session
print(f"[get_current_user] 查询活跃 Session: user_id={user.id}, device_id={device_id}")
session = db.query(UserSession).filter(
UserSession.user_id == user.id,
UserSession.device_id == device_id,
UserSession.active == True
).first()
if not session:
# Session 不存在或已失效(被踢下线/登出)
print(f"[get_current_user] ✗ Session 不存在或已失效")
# 调试:列出该用户的所有 Session
all_sessions = db.query(UserSession).filter(UserSession.user_id == user.id).all()
print(f"[get_current_user] 该用户共有 {len(all_sessions)} 个 Session:")
for s in all_sessions:
print(f" - session_id={s.id}, device_id={s.device_id}, active={s.active}")
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="会话已失效或在其他设备登录",
headers={"WWW-Authenticate": "Bearer"},
)
print(f"[get_current_user] ✓ Session 验证通过: session_id={session.id}")
else:
print(f"[get_current_user] ⚠️ Token 中没有 device_id跳过 Session 校验")
print("="*60)
return username