feat: migrate ORM base to sqlmodel-ext, add file viewers and WOPI integration
- Migrate SQLModel base classes, mixins, and database management to external sqlmodel-ext package; remove sqlmodels/base/, sqlmodels/mixin/, and sqlmodels/database.py - Add file viewer/editor system with WOPI protocol support for collaborative editing (OnlyOffice, Collabora) - Add enterprise edition license verification module (ee/) - Add Dockerfile multi-stage build with Cython compilation support - Add new dependencies: sqlmodel-ext, cryptography, whatthepatch Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -25,11 +25,11 @@ async def load_secret_key() -> None:
|
||||
从数据库读取 JWT 的密钥。
|
||||
"""
|
||||
# 延迟导入以避免循环依赖
|
||||
from sqlmodels.database import get_session
|
||||
from sqlmodels.database_connection import DatabaseManager
|
||||
from sqlmodels.setting import Setting
|
||||
|
||||
global SECRET_KEY
|
||||
async for session in get_session():
|
||||
async for session in DatabaseManager.get_session():
|
||||
setting: Setting = await Setting.get(
|
||||
session,
|
||||
(Setting.type == "auth") & (Setting.name == "secret_key")
|
||||
|
||||
67
utils/JWT/wopi_token.py
Normal file
67
utils/JWT/wopi_token.py
Normal file
@@ -0,0 +1,67 @@
|
||||
"""
|
||||
WOPI 访问令牌生成与验证。
|
||||
|
||||
使用 JWT 签名,payload 包含 file_id, user_id, can_write, exp。
|
||||
TTL 默认 10 小时(WOPI 规范推荐长 TTL)。
|
||||
"""
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
import jwt
|
||||
|
||||
from sqlmodels.wopi import WopiAccessTokenPayload
|
||||
|
||||
WOPI_TOKEN_TTL = timedelta(hours=10)
|
||||
"""WOPI 令牌有效期"""
|
||||
|
||||
|
||||
def create_wopi_token(
|
||||
file_id: UUID,
|
||||
user_id: UUID,
|
||||
can_write: bool = False,
|
||||
) -> tuple[str, int]:
|
||||
"""
|
||||
创建 WOPI 访问令牌。
|
||||
|
||||
:param file_id: 文件UUID
|
||||
:param user_id: 用户UUID
|
||||
:param can_write: 是否可写
|
||||
:return: (token_string, access_token_ttl_ms)
|
||||
"""
|
||||
from utils.JWT import SECRET_KEY
|
||||
|
||||
expire = datetime.now(timezone.utc) + WOPI_TOKEN_TTL
|
||||
payload = {
|
||||
"jti": str(uuid4()),
|
||||
"file_id": str(file_id),
|
||||
"user_id": str(user_id),
|
||||
"can_write": can_write,
|
||||
"exp": expire,
|
||||
"type": "wopi",
|
||||
}
|
||||
token = jwt.encode(payload, SECRET_KEY, algorithm="HS256")
|
||||
# WOPI 规范要求 access_token_ttl 是毫秒级的 UNIX 时间戳
|
||||
access_token_ttl = int(expire.timestamp() * 1000)
|
||||
return token, access_token_ttl
|
||||
|
||||
|
||||
def verify_wopi_token(token: str) -> WopiAccessTokenPayload | None:
|
||||
"""
|
||||
验证 WOPI 访问令牌并返回 payload。
|
||||
|
||||
:param token: JWT 令牌字符串
|
||||
:return: WopiAccessTokenPayload 或 None(验证失败)
|
||||
"""
|
||||
from utils.JWT import SECRET_KEY
|
||||
|
||||
try:
|
||||
payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
|
||||
if payload.get("type") != "wopi":
|
||||
return None
|
||||
return WopiAccessTokenPayload(
|
||||
file_id=UUID(payload["file_id"]),
|
||||
user_id=UUID(payload["user_id"]),
|
||||
can_write=payload.get("can_write", False),
|
||||
)
|
||||
except (jwt.InvalidTokenError, KeyError, ValueError):
|
||||
return None
|
||||
@@ -40,6 +40,10 @@ def raise_conflict(detail: str | None = None) -> NoReturn:
|
||||
"""Raises an HTTP 409 Conflict exception."""
|
||||
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=detail)
|
||||
|
||||
def raise_unprocessable_entity(detail: str | None = None) -> NoReturn:
|
||||
"""Raises an HTTP 422 Unprocessable Content exception."""
|
||||
raise HTTPException(status_code=422, detail=detail)
|
||||
|
||||
def raise_precondition_required(detail: str | None = None) -> NoReturn:
|
||||
"""Raises an HTTP 428 Precondition required exception."""
|
||||
raise HTTPException(status_code=status.HTTP_428_PRECONDITION_REQUIRED, detail=detail)
|
||||
|
||||
Reference in New Issue
Block a user