modify: auth插件拆分,将token签发部分交给网关
This commit is contained in:
@@ -328,7 +328,7 @@ inline worker 的生命周期交给**基于分布式锁的宿主级领导选举*
|
|||||||
|
|
||||||
`Worker 管理`(`/workers`,深色主题):
|
`Worker 管理`(`/workers`,深色主题):
|
||||||
|
|
||||||
- **登录**:经 auth 插件 `POST /auth/login` 领取管理 Token(无需手填 JWT),从 JWT 解析当前用户与超管标识。
|
- **登录**:内置网关端点 `POST /auth/login`(auth 域拆分后登录/签发不依赖 auth 插件,插件缩容或故障时登录与 admin API 仍可用),领取管理 Token(无需手填 JWT),从 JWT 解析当前用户与超管标识。
|
||||||
- **鉴权**:仅 `is_superuser=true` 可管理;权限不足时前端/后端弹出对应提示(403)。
|
- **鉴权**:仅 `is_superuser=true` 可管理;权限不足时前端/后端弹出对应提示(403)。
|
||||||
- **列表分页**:展示副本(插件类型 / 副本 ID / 模式 inline|external / 状态),内置分页。
|
- **列表分页**:展示副本(插件类型 / 副本 ID / 模式 inline|external / 状态),内置分页。
|
||||||
- **查询与筛选**:列表顶部统一筛选栏——关键字搜索(插件类型 / 副本 ID / 实例 ID)、模式筛选(全部 / inline / external)、副本数 ≥ N 过滤;**同时作用于下方「逐实例精细扩缩」分组表**(副本数过滤仅对分组表生效),筛选条件变化自动回到第一页。
|
- **查询与筛选**:列表顶部统一筛选栏——关键字搜索(插件类型 / 副本 ID / 实例 ID)、模式筛选(全部 / inline / external)、副本数 ≥ N 过滤;**同时作用于下方「逐实例精细扩缩」分组表**(副本数过滤仅对分组表生效),筛选条件变化自动回到第一页。
|
||||||
@@ -729,7 +729,8 @@ uv pip install -e utils/<name> # 安装/更新工具包
|
|||||||
|
|
||||||
| 插件 | 路径 | 功能 |
|
| 插件 | 路径 | 功能 |
|
||||||
| ------------------------ | ----------------------------------- | ------------------------------------------------------- |
|
| ------------------------ | ----------------------------------- | ------------------------------------------------------- |
|
||||||
| **auth** | `src/plugins/auth/` | 用户注册/登录/资料管理,JWT 鉴权,管理员初始化 |
|
| **auth** | `src/plugins/auth/` | 用户管理插件:注册/资料/改密(登录与 token 签发已内置网关,Users 模型/种子/校验/签发共享于 `chongming-auth` 包) |
|
||||||
|
| **auth_oauth** | `src/plugins/auth_oauth/` | 身份源插件示例:GitHub OAuth + 手机验证码登录,基于 `chongming-auth` 契约签发平台 token |
|
||||||
| **doubao** | `src/plugins/doubao/` | 豆包视觉理解,支持 AICC 安全通信通道 |
|
| **doubao** | `src/plugins/doubao/` | 豆包视觉理解,支持 AICC 安全通信通道 |
|
||||||
| **aibox_sophon_adapter** | `src/plugins/aibox_sophon_adapter/` | AI Box 设备心跳监控、离线检测、告警上报与查询、视频上传 |
|
| **aibox_sophon_adapter** | `src/plugins/aibox_sophon_adapter/` | AI Box 设备心跳监控、离线检测、告警上报与查询、视频上传 |
|
||||||
| **example** | `src/plugins/example/` | 四则运算 + 心跳演示(含 MQTT 端点示例) |
|
| **example** | `src/plugins/example/` | 四则运算 + 心跳演示(含 MQTT 端点示例) |
|
||||||
@@ -741,6 +742,7 @@ uv pip install -e utils/<name> # 安装/更新工具包
|
|||||||
|
|
||||||
| 包名 | 路径 | 语言 | 说明 |
|
| 包名 | 路径 | 语言 | 说明 |
|
||||||
| ---------------------------- | ----------------------- | ----------- | ---------------------------------------------- |
|
| ---------------------------- | ----------------------- | ----------- | ---------------------------------------------- |
|
||||||
|
| **chongming-auth** | `utils/auth/` | Python | auth 域共享契约:Users 模型、建库/种子、登录校验与 JWT 签发(网关内置登录与 auth 插件共用) |
|
||||||
| **chongming-config** | `utils/config/` | Python | TOML 配置加载,TypedDict 类型安全 |
|
| **chongming-config** | `utils/config/` | Python | TOML 配置加载,TypedDict 类型安全 |
|
||||||
| **chongming-database** | `utils/database/` | Python | aiosqlite + SQLModel 异步数据库封装 |
|
| **chongming-database** | `utils/database/` | Python | aiosqlite + SQLModel 异步数据库封装 |
|
||||||
| **chongming-jwt** | `utils/jwt/` | Rust + PyO3 | JWT 编解码,支持 HS256/384/512 + RS256/384/512 |
|
| **chongming-jwt** | `utils/jwt/` | Rust + PyO3 | JWT 编解码,支持 HS256/384/512 + RS256/384/512 |
|
||||||
@@ -837,7 +839,7 @@ uv pip install -e utils/<name> # 安装/更新工具包子包
|
|||||||
|
|
||||||
| 插件 | HTTP 前缀 | MQTT 主题前缀 | 说明 |
|
| 插件 | HTTP 前缀 | MQTT 主题前缀 | 说明 |
|
||||||
| ------------------------ | --------- | ------------------------ | ------------------------------------ |
|
| ------------------------ | --------- | ------------------------ | ------------------------------------ |
|
||||||
| **auth** | `/auth` | `auth.*` | 注册、登录、JWT 鉴权、个人信息、改密 |
|
| **auth** | `/auth` | `auth.*` | 注册、个人信息、改密(登录 `POST /auth/login` 内置网关) |
|
||||||
| **doubao** | `/doubao` | `doubao.*` | 火山引擎豆包视觉理解模型 |
|
| **doubao** | `/doubao` | `doubao.*` | 火山引擎豆包视觉理解模型 |
|
||||||
| **example** | `/calc` | `example.*` | 四则运算 + 访问计数 + MQTT 心跳示例 |
|
| **example** | `/calc` | `example.*` | 四则运算 + 访问计数 + MQTT 心跳示例 |
|
||||||
| **aibox_sophon_adapter** | `/aibox` | `aibox_sophon_adapter.*` | AI Box 设备心跳适配 + 视频上传 |
|
| **aibox_sophon_adapter** | `/aibox` | `aibox_sophon_adapter.*` | AI Box 设备心跳适配 + 视频上传 |
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ dependencies = [
|
|||||||
"httptools",
|
"httptools",
|
||||||
"bytedance",
|
"bytedance",
|
||||||
"chongming-cache",
|
"chongming-cache",
|
||||||
|
"chongming-auth",
|
||||||
]
|
]
|
||||||
description = "基于 modules-bank 的 fastapi 打包脚手架"
|
description = "基于 modules-bank 的 fastapi 打包脚手架"
|
||||||
name = "chongming-zenoh"
|
name = "chongming-zenoh"
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ from chongming_config import get_config
|
|||||||
from .core.logger import get_logger
|
from .core.logger import get_logger
|
||||||
from .core.static_files import SVFSStaticFiles
|
from .core.static_files import SVFSStaticFiles
|
||||||
from .core import lifespan_setup, call_stats
|
from .core import lifespan_setup, call_stats
|
||||||
from .routes import storage_router, stats_router, worker_admin_router
|
from .routes import storage_router, stats_router, worker_admin_router, auth_router
|
||||||
|
|
||||||
|
|
||||||
config = get_config()
|
config = get_config()
|
||||||
@@ -39,6 +39,11 @@ async def lifespan(app: FastAPI):
|
|||||||
# 1. 本地存储(替代 Nanio S3,省 ~172 MB)
|
# 1. 本地存储(替代 Nanio S3,省 ~172 MB)
|
||||||
lifespan_setup.setup_storage(app)
|
lifespan_setup.setup_storage(app)
|
||||||
|
|
||||||
|
# 1.2 内置 auth 库(users 表 + admin 种子)—— auth 域的基础设施部分:
|
||||||
|
# 登录/签发内置在网关(管理平面底线),不依赖 auth 插件是否加载;
|
||||||
|
# 与 auth 插件(用户管理)共用同一张 users 表
|
||||||
|
await lifespan_setup.setup_auth_store(app)
|
||||||
|
|
||||||
# 2. Zenoh
|
# 2. Zenoh
|
||||||
session = lifespan_setup.setup_zenoh(app)
|
session = lifespan_setup.setup_zenoh(app)
|
||||||
|
|
||||||
@@ -123,6 +128,11 @@ async def lifespan(app: FastAPI):
|
|||||||
# 2. 清理 inline workers(cleanup_workers 内部按 load_mode 过滤)
|
# 2. 清理 inline workers(cleanup_workers 内部按 load_mode 过滤)
|
||||||
await lifespan_setup.cleanup_workers(app)
|
await lifespan_setup.cleanup_workers(app)
|
||||||
|
|
||||||
|
# 2.2 关闭内置 auth 库
|
||||||
|
auth_db = getattr(app.state, "auth_db", None)
|
||||||
|
if auth_db is not None:
|
||||||
|
await auth_db.dispose()
|
||||||
|
|
||||||
# 2.1 释放 inline worker 所有权分布式锁(仅持锁进程,避免误删其他进程获得的新锁)
|
# 2.1 释放 inline worker 所有权分布式锁(仅持锁进程,避免误删其他进程获得的新锁)
|
||||||
if getattr(app.state, "inline_owner_locked", False):
|
if getattr(app.state, "inline_owner_locked", False):
|
||||||
await manager.release_inline_owner()
|
await manager.release_inline_owner()
|
||||||
@@ -303,3 +313,4 @@ if vfs_db_path := config[env].get("file_system", {}).get("path", None):
|
|||||||
app.include_router(storage_router)
|
app.include_router(storage_router)
|
||||||
app.include_router(stats_router)
|
app.include_router(stats_router)
|
||||||
app.include_router(worker_admin_router)
|
app.include_router(worker_admin_router)
|
||||||
|
app.include_router(auth_router)
|
||||||
|
|||||||
@@ -244,6 +244,33 @@ def setup_storage(app: FastAPI):
|
|||||||
app.state.upload_root = upload_root
|
app.state.upload_root = upload_root
|
||||||
logger.info(f"本地存储已就绪: {upload_root}")
|
logger.info(f"本地存储已就绪: {upload_root}")
|
||||||
|
|
||||||
|
|
||||||
|
async def setup_auth_store(app: FastAPI) -> None:
|
||||||
|
"""初始化内置 auth 库(users 表 + admin 种子)—— auth 域的基础设施部分。
|
||||||
|
|
||||||
|
拆分自 plugins.auth:登录/签发内置在网关(管理平面底线),auth 插件
|
||||||
|
缩容到 0 或全部副本故障时登录与 admin API 仍可用。库位置与 auth 插件
|
||||||
|
读同一份配置(config/workers/auth/config.toml 的 [workers.config]:
|
||||||
|
data_dir / database),指向同一张 users 表;插件配置缺失(如彻底移除
|
||||||
|
auth 插件)时回退默认 ./data/auth.db。
|
||||||
|
"""
|
||||||
|
from chongming_auth import open_database, seed_admin
|
||||||
|
|
||||||
|
workers_conf = {}
|
||||||
|
auth_conf_path = "config/workers/auth/config.toml"
|
||||||
|
if os.path.isfile(auth_conf_path):
|
||||||
|
try:
|
||||||
|
with open(auth_conf_path, "rb") as f:
|
||||||
|
workers_conf = tomllib.load(f).get("workers", {}).get("config", {})
|
||||||
|
except Exception as e: # noqa: BLE001 - 配置损坏不阻断启动,走默认路径
|
||||||
|
logger.warning(f"无法读取 auth 插件配置 {auth_conf_path}: {e}(使用默认 data_dir)")
|
||||||
|
|
||||||
|
db = await open_database(workers_conf)
|
||||||
|
created = await seed_admin(db)
|
||||||
|
app.state.auth_db = db
|
||||||
|
logger.info(f"内置 auth 库已就绪 (admin 种子: {'新建' if created else '已存在'})")
|
||||||
|
|
||||||
|
|
||||||
def setup_zenoh(app: FastAPI) -> zenoh.Session:
|
def setup_zenoh(app: FastAPI) -> zenoh.Session:
|
||||||
"""建立 Zenoh 会话并存入 app.state。"""
|
"""建立 Zenoh 会话并存入 app.state。"""
|
||||||
zenoh_config = config.get("zenoh", {})
|
zenoh_config = config.get("zenoh", {})
|
||||||
@@ -724,7 +751,7 @@ async def setup_mqtt(app: FastAPI, worker_manager: WorkerManager) -> MQTTBridge
|
|||||||
# 排他锁:基于共享缓存(redis / 共享卷 diskcache),保证全局只有一个 Bridge 消费。
|
# 排他锁:基于共享缓存(redis / 共享卷 diskcache),保证全局只有一个 Bridge 消费。
|
||||||
# 相比原文件锁,可跨多台主机收敛——多实例共享同一个外部 Broker 时,
|
# 相比原文件锁,可跨多台主机收敛——多实例共享同一个外部 Broker 时,
|
||||||
# 避免一条 MQTT 请求被 N 台网关上各自消费、各回一份响应。
|
# 避免一条 MQTT 请求被 N 台网关上各自消费、各回一份响应。
|
||||||
bridge_lock = DistributedLock(worker_manager.cache, "mqtt_bridge")
|
bridge_lock = DistributedLock(worker_manager.cache, "mqtt_bridge") # pyright: ignore[reportArgumentType]
|
||||||
if not bridge_lock.acquire():
|
if not bridge_lock.acquire():
|
||||||
logger.info(
|
logger.info(
|
||||||
"MQTT Bridge 已由其他网关实例持有(分布式锁),本网关跳过,"
|
"MQTT Bridge 已由其他网关实例持有(分布式锁),本网关跳过,"
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from .storage import router as storage_router
|
from .storage import router as storage_router
|
||||||
from .stats import router as stats_router
|
from .stats import router as stats_router
|
||||||
from .worker_admin import router as worker_admin_router
|
from .worker_admin import router as worker_admin_router
|
||||||
|
from .auth import router as auth_router
|
||||||
|
|
||||||
__all__ = ["storage_router", "stats_router", "worker_admin_router"]
|
__all__ = ["storage_router", "stats_router", "worker_admin_router", "auth_router"]
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
"""内置登录端点 —— auth 域的基础设施部分(管理平面可用性底线)。
|
||||||
|
|
||||||
|
拆分自 plugins.auth:登录 / token 签发不再依赖 auth 插件是否加载,
|
||||||
|
auth 插件缩容到 0 或全部副本故障时,登录与 admin API 仍可用;
|
||||||
|
用户管理功能(注册 / 资料 / 改密)仍由 auth 插件提供。
|
||||||
|
|
||||||
|
共享契约(Users 模型 / claims / 失败文案)来自 chongming_auth 包,
|
||||||
|
路径与响应 shape 与拆分前完全一致(前端零改动)。
|
||||||
|
"""
|
||||||
|
import time
|
||||||
|
|
||||||
|
from chongming_auth import (
|
||||||
|
DEFAULT_TOKEN_EXPIRES_SECONDS,
|
||||||
|
LoginPostRequest,
|
||||||
|
LoginPostResponse,
|
||||||
|
issue_token,
|
||||||
|
touch_last_login,
|
||||||
|
verify_login,
|
||||||
|
)
|
||||||
|
from chongming_config import get_config
|
||||||
|
from fastapi import APIRouter, Request
|
||||||
|
|
||||||
|
config = get_config()
|
||||||
|
|
||||||
|
router = APIRouter(prefix=config["default"]["prefix"], tags=["auth"])
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"/auth/login",
|
||||||
|
response_model=LoginPostResponse,
|
||||||
|
summary="登录",
|
||||||
|
description="根据用户名和密码进行登录,登录成功返回token,获取请求需带式token进行请求",
|
||||||
|
)
|
||||||
|
async def login(body: LoginPostRequest, request: Request) -> LoginPostResponse:
|
||||||
|
db = request.app.state.auth_db
|
||||||
|
user, message = await verify_login(db, body.username, body.password)
|
||||||
|
if user is None:
|
||||||
|
return LoginPostResponse(
|
||||||
|
result="fail", token="", message=message, timestamp=time.time()
|
||||||
|
)
|
||||||
|
|
||||||
|
await touch_last_login(db, user)
|
||||||
|
|
||||||
|
security = get_config()["security"]
|
||||||
|
token = issue_token(
|
||||||
|
user,
|
||||||
|
security["secret_key"],
|
||||||
|
security["algorithm"],
|
||||||
|
expires_in_seconds=DEFAULT_TOKEN_EXPIRES_SECONDS,
|
||||||
|
)
|
||||||
|
return LoginPostResponse(
|
||||||
|
result="success", token=token, message="登录成功", timestamp=time.time()
|
||||||
|
)
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
"""
|
||||||
|
内置登录端到端测试(auth 域拆分验证)
|
||||||
|
|
||||||
|
验证拆分目标:POST /api/v1/auth/login 内置在网关,不依赖 auth 插件——
|
||||||
|
本测试只挂载内置 auth 路由(不加载任何插件),登录/签发/响应 shape 全部可用。
|
||||||
|
|
||||||
|
同时校验与拆分前 plugins.auth.login 的兼容性:
|
||||||
|
- 路径 /api/v1/auth/login、响应字段 {result, token, message, timestamp}
|
||||||
|
- 失败文案逐字一致(用户名错误 / 密码错误)
|
||||||
|
- token 可被 chongming_jwt 解密回读,claims 含 is_superuser
|
||||||
|
(网关 admin API 判超管的契约)
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import orjson
|
||||||
|
import pytest
|
||||||
|
from chongming_auth import open_database, seed_admin
|
||||||
|
from chongming_config import get_config
|
||||||
|
from chongming_jwt import JWT
|
||||||
|
from fastapi import FastAPI
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
from chongming.app.routes.auth import router as auth_router
|
||||||
|
|
||||||
|
_SECURITY = get_config()["security"]
|
||||||
|
SECRET, ALGORITHM = _SECURITY["secret_key"], _SECURITY["algorithm"]
|
||||||
|
PREFIX = get_config()["default"]["prefix"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def client(tmp_path: Path):
|
||||||
|
"""最小 app:仅内置 auth 路由 + 临时 auth 库(模拟 auth 插件完全缺席)。"""
|
||||||
|
async def _build():
|
||||||
|
db = await open_database({"data_dir": str(tmp_path)})
|
||||||
|
await seed_admin(db)
|
||||||
|
return db
|
||||||
|
|
||||||
|
db = asyncio.run(_build())
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
app.include_router(auth_router)
|
||||||
|
app.state.auth_db = db
|
||||||
|
with TestClient(app) as c:
|
||||||
|
yield c
|
||||||
|
asyncio.run(db.dispose())
|
||||||
|
|
||||||
|
|
||||||
|
def test_login_success_shape_and_claims(client):
|
||||||
|
resp = client.post(
|
||||||
|
f"{PREFIX}/auth/login", json={"username": "admin", "password": "admin"}
|
||||||
|
)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
data = resp.json()
|
||||||
|
# 响应 shape 与拆分前 gen_models 产物一致
|
||||||
|
assert set(data.keys()) == {"result", "token", "message", "timestamp"}
|
||||||
|
assert data["result"] == "success"
|
||||||
|
assert data["message"] == "登录成功"
|
||||||
|
assert isinstance(data["timestamp"], float)
|
||||||
|
assert data["timestamp"] == pytest.approx(time.time(), abs=60)
|
||||||
|
|
||||||
|
# token 可解密且 claims 契约完整(is_superuser 是 admin API 的判超管依据)
|
||||||
|
payload = orjson.loads(JWT(SECRET, ALGORITHM).decode(data["token"]))
|
||||||
|
assert payload["sub"]
|
||||||
|
assert payload["username"] == "admin"
|
||||||
|
assert payload["is_superuser"] is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_login_failure_messages_compat(client):
|
||||||
|
"""失败文案与拆分前 plugins.auth.login 逐字一致。"""
|
||||||
|
for body, expected_msg in [
|
||||||
|
({"username": "nouser", "password": "x"}, "用户名错误"),
|
||||||
|
({"username": "admin", "password": "wrong"}, "密码错误"),
|
||||||
|
]:
|
||||||
|
resp = client.post(f"{PREFIX}/auth/login", json=body)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
data = resp.json()
|
||||||
|
assert data["result"] == "fail"
|
||||||
|
assert data["token"] == ""
|
||||||
|
assert data["message"] == expected_msg
|
||||||
|
assert isinstance(data["timestamp"], float)
|
||||||
|
|
||||||
|
|
||||||
|
def test_login_works_without_auth_plugin(client):
|
||||||
|
"""拆分核心目标:无 auth 插件时登录照常(fixture 未加载任何插件)。"""
|
||||||
|
resp = client.post(
|
||||||
|
f"{PREFIX}/auth/login", json={"username": "admin", "password": "admin"}
|
||||||
|
)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert resp.json()["result"] == "success"
|
||||||
|
|
||||||
|
|
||||||
|
def test_login_updates_last_login(client, tmp_path):
|
||||||
|
"""登录成功后 last_login_at 落库(顺带修复拆分前未 commit 的旧问题)。"""
|
||||||
|
client.post(f"{PREFIX}/auth/login", json={"username": "admin", "password": "admin"})
|
||||||
|
|
||||||
|
row = asyncio.run(_read_admin_row(tmp_path))
|
||||||
|
assert row.last_login_at is not None
|
||||||
|
|
||||||
|
|
||||||
|
async def _read_admin_row(data_dir: Path):
|
||||||
|
from chongming_auth import Users
|
||||||
|
from sqlalchemy import select
|
||||||
|
|
||||||
|
db = await open_database({"data_dir": str(data_dir)})
|
||||||
|
try:
|
||||||
|
async with db.get_session() as sess:
|
||||||
|
return (
|
||||||
|
await sess.execute(select(Users).filter_by(username="admin"))
|
||||||
|
).scalar_one()
|
||||||
|
finally:
|
||||||
|
await db.dispose()
|
||||||
@@ -0,0 +1,215 @@
|
|||||||
|
"""
|
||||||
|
auth_oauth 身份源插件测试(基于 chongming-auth 共享契约的第三方登录示例)
|
||||||
|
|
||||||
|
覆盖:
|
||||||
|
1. OTP 手机验证码登录全流程(演示模式 dev_code):
|
||||||
|
发码 → 错码拒绝 → 正确码登录签发平台 token → 同号复登复用同一账号(绑定生效)
|
||||||
|
2. GitHub OAuth 授权码流程(HTTP 调用打桩):
|
||||||
|
authorize 生成 URL+state → callback find-or-create 签发 token → state 一次性(重放拒绝)
|
||||||
|
3. 绑定表与共享 users 表落库验证(chongming_auth.Users 可查)
|
||||||
|
4. 插件导入 + 三方契约校验 0 错误
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import orjson
|
||||||
|
import pytest
|
||||||
|
from chongming_auth import Users, open_database
|
||||||
|
from chongming_cache import Cache
|
||||||
|
from chongming_config import get_config
|
||||||
|
from chongming_jwt import JWT
|
||||||
|
from chongming_worker.contracts import validate_worker_contracts
|
||||||
|
|
||||||
|
# 与被测插件同源:issue_token 从 get_config()["security"] 读密钥,测试解码必须一致
|
||||||
|
_SECURITY = get_config()["security"]
|
||||||
|
SECRET, ALGORITHM = _SECURITY["secret_key"], _SECURITY["algorithm"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def env(tmp_path: Path):
|
||||||
|
"""初始化插件运行环境:共享 auth 库 + 临时 sqlite 缓存 + global_var 注入。
|
||||||
|
|
||||||
|
注意:插件模块 @rpc_method 注册在进程内只执行一次,fixture 不清空注册表
|
||||||
|
(clear_registry 会导致后续测试 import no-op、注册表为空)。
|
||||||
|
"""
|
||||||
|
import plugins.auth_oauth # noqa: F401 触发 @rpc_method 注册(幂等)
|
||||||
|
import plugins.auth_oauth.global_var as gv
|
||||||
|
|
||||||
|
async def _build():
|
||||||
|
db = await open_database({"data_dir": str(tmp_path)})
|
||||||
|
cache = Cache(backend="sqlite", path=str(tmp_path / "cache.sqlite"))
|
||||||
|
return db, cache
|
||||||
|
|
||||||
|
db, cache = asyncio.run(_build())
|
||||||
|
gv.set_global_var("db", db)
|
||||||
|
gv.set_global_var("cache", cache)
|
||||||
|
gv.set_global_var("plugin_config", {
|
||||||
|
"return_otp_in_response": True,
|
||||||
|
"github": {
|
||||||
|
"client_id": "test-client-id",
|
||||||
|
"client_secret": "test-client-secret",
|
||||||
|
"redirect_uri": "http://localhost:5173/auth/oauth/github/callback",
|
||||||
|
"scope": "read:user user:email",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
yield gv
|
||||||
|
|
||||||
|
async def _teardown():
|
||||||
|
await db.dispose()
|
||||||
|
cache.close()
|
||||||
|
|
||||||
|
asyncio.run(_teardown())
|
||||||
|
|
||||||
|
|
||||||
|
def _decode(token: str) -> dict:
|
||||||
|
return orjson.loads(JWT(SECRET, ALGORITHM).decode(token))
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- 手机验证码登录 ----------
|
||||||
|
|
||||||
|
def test_otp_full_flow(env):
|
||||||
|
from plugins.auth_oauth.router.phone import (
|
||||||
|
PhoneLoginPostRequest,
|
||||||
|
PhoneSendCodePostRequest,
|
||||||
|
phone_login,
|
||||||
|
phone_send_code,
|
||||||
|
)
|
||||||
|
|
||||||
|
phone = "13800138000"
|
||||||
|
|
||||||
|
# 1. 发码(演示模式返回 dev_code)
|
||||||
|
sent = asyncio.run(phone_send_code(PhoneSendCodePostRequest(phone=phone)))
|
||||||
|
assert sent.result == "success" and sent.dev_code
|
||||||
|
|
||||||
|
# 2. 错码拒绝
|
||||||
|
wrong = asyncio.run(phone_login(
|
||||||
|
PhoneLoginPostRequest(phone=phone, code="000000" if sent.dev_code != "000000" else "111111")
|
||||||
|
))
|
||||||
|
assert wrong.result == "fail" and wrong.token == ""
|
||||||
|
|
||||||
|
# 3. 正确码登录 → 平台 token
|
||||||
|
ok = asyncio.run(phone_login(
|
||||||
|
PhoneLoginPostRequest(phone=phone, code=sent.dev_code)
|
||||||
|
))
|
||||||
|
assert ok.result == "success" and ok.token
|
||||||
|
claims = _decode(ok.token)
|
||||||
|
assert claims["username"] == phone
|
||||||
|
assert claims["is_superuser"] is False
|
||||||
|
|
||||||
|
# 4. 验证码一次性
|
||||||
|
replay = asyncio.run(phone_login(
|
||||||
|
PhoneLoginPostRequest(phone=phone, code=sent.dev_code)
|
||||||
|
))
|
||||||
|
assert replay.result == "fail"
|
||||||
|
|
||||||
|
# 5. 复登(再次发码登录)→ 复用同一账号(绑定生效,不重复建号)
|
||||||
|
sent2 = asyncio.run(phone_send_code(PhoneSendCodePostRequest(phone=phone)))
|
||||||
|
ok2 = asyncio.run(phone_login(
|
||||||
|
PhoneLoginPostRequest(phone=phone, code=sent2.dev_code)
|
||||||
|
))
|
||||||
|
assert _decode(ok2.token)["sub"] == claims["sub"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_otp_invalid_phone(env):
|
||||||
|
from plugins.auth_oauth.router.phone import (
|
||||||
|
PhoneSendCodePostRequest, phone_send_code,
|
||||||
|
)
|
||||||
|
|
||||||
|
resp = asyncio.run(phone_send_code(PhoneSendCodePostRequest(phone="abc")))
|
||||||
|
assert resp.result == "fail" and "手机号" in resp.message
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- GitHub OAuth ----------
|
||||||
|
|
||||||
|
def test_github_authorize_and_callback(env, monkeypatch):
|
||||||
|
from plugins.auth_oauth.router.github import (
|
||||||
|
GithubCallbackPostRequest, github_authorize, github_callback,
|
||||||
|
)
|
||||||
|
from plugins.auth_oauth.providers import github as github_provider
|
||||||
|
|
||||||
|
# authorize:URL 含 client_id 与 state,state 已入缓存
|
||||||
|
authz = asyncio.run(github_authorize())
|
||||||
|
assert authz.result == "success"
|
||||||
|
assert "client_id=test-client-id" in authz.authorize_url
|
||||||
|
assert f"state={authz.state}" in authz.authorize_url
|
||||||
|
assert env.cache.get(f"auth_oauth:state:{authz.state}")
|
||||||
|
|
||||||
|
# 打桩:授权码换取与用户拉取(免外网)
|
||||||
|
fake_profile = {
|
||||||
|
"uid": "97594220", "username": "chongming-demo",
|
||||||
|
"email": "demo@example.com", "full_name": "Chongming Demo",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/97594220",
|
||||||
|
}
|
||||||
|
exchanged = {}
|
||||||
|
|
||||||
|
async def fake_exchange(client_id, client_secret, code, redirect_uri):
|
||||||
|
exchanged["code"] = code
|
||||||
|
return "fake-access-token"
|
||||||
|
|
||||||
|
async def fake_fetch(access_token):
|
||||||
|
assert access_token == "fake-access-token"
|
||||||
|
return dict(fake_profile)
|
||||||
|
|
||||||
|
monkeypatch.setattr(github_provider, "exchange_code", fake_exchange)
|
||||||
|
monkeypatch.setattr(github_provider, "fetch_user", fake_fetch)
|
||||||
|
|
||||||
|
resp = asyncio.run(github_callback(
|
||||||
|
GithubCallbackPostRequest(code="gh-code", state=authz.state)
|
||||||
|
))
|
||||||
|
assert resp.result == "success" and resp.token
|
||||||
|
claims = _decode(resp.token)
|
||||||
|
assert claims["username"] == "chongming-demo"
|
||||||
|
assert claims["email"] == "demo@example.com"
|
||||||
|
assert exchanged["code"] == "gh-code"
|
||||||
|
|
||||||
|
# state 一次性:重放拒绝
|
||||||
|
replay = asyncio.run(github_callback(
|
||||||
|
GithubCallbackPostRequest(code="gh-code", state=authz.state)
|
||||||
|
))
|
||||||
|
assert replay.result == "fail" and "state" in replay.message
|
||||||
|
|
||||||
|
# 落库验证:users 表有新账号 + 绑定指向同一账号;再次登录(新 state)复用
|
||||||
|
async def _read_users():
|
||||||
|
db = env.db
|
||||||
|
from sqlalchemy import select
|
||||||
|
|
||||||
|
async with db.get_session() as sess:
|
||||||
|
return (await sess.execute(select(Users))).scalars().all()
|
||||||
|
|
||||||
|
users = asyncio.run(_read_users())
|
||||||
|
assert len(users) == 1
|
||||||
|
assert users[0].email == "demo@example.com"
|
||||||
|
|
||||||
|
async def _second_login():
|
||||||
|
from plugins.auth_oauth.router.github import github_authorize as _a
|
||||||
|
|
||||||
|
a = await _a()
|
||||||
|
return await github_callback(
|
||||||
|
GithubCallbackPostRequest(code="gh-code-2", state=a.state)
|
||||||
|
)
|
||||||
|
|
||||||
|
second = asyncio.run(_second_login())
|
||||||
|
assert _decode(second.token)["sub"] == claims["sub"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_github_callback_bad_state(env):
|
||||||
|
from plugins.auth_oauth.router.github import (
|
||||||
|
GithubCallbackPostRequest, github_callback,
|
||||||
|
)
|
||||||
|
|
||||||
|
resp = asyncio.run(github_callback(
|
||||||
|
GithubCallbackPostRequest(code="x", state="not-exist")
|
||||||
|
))
|
||||||
|
assert resp.result == "fail" and "state" in resp.message
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- 契约校验 ----------
|
||||||
|
|
||||||
|
def test_contract_validation(env):
|
||||||
|
import plugins.auth_oauth # noqa: F401 确保注册
|
||||||
|
|
||||||
|
issues = validate_worker_contracts("auth_oauth")
|
||||||
|
errors = [i for i in issues if i.get("level") == "error"]
|
||||||
|
assert errors == [], f"契约校验存在错误: {errors}"
|
||||||
+11
-25
@@ -1,37 +1,25 @@
|
|||||||
"""
|
"""
|
||||||
并发启动竞态测试:模拟 gunicorn 多 worker 同时启动时,
|
并发启动竞态测试:模拟 gunicorn 多 worker 同时启动时,
|
||||||
多个进程并发调用 plugins.auth.create_workers 初始化默认管理员。
|
多个进程并发执行网关内置的 admin 种子(chongming_auth.seed_admin)。
|
||||||
|
|
||||||
|
auth 域拆分后,admin 种子从 plugins.auth 的 on_init 迁移到网关
|
||||||
|
lifespan(setup_auth_store → chongming_auth.seed_admin),本测试随之改指向。
|
||||||
|
|
||||||
预期:不抛 IntegrityError,users 表中只有一条 admin 记录。
|
预期:不抛 IntegrityError,users 表中只有一条 admin 记录。
|
||||||
"""
|
"""
|
||||||
import asyncio
|
import asyncio
|
||||||
import sys
|
|
||||||
import tempfile
|
import tempfile
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
sys.path.insert(0, str(Path(__file__).parent / "src"))
|
from chongming_auth import open_database, seed_admin
|
||||||
|
|
||||||
import plugins.auth as auth_plugin
|
|
||||||
|
|
||||||
|
|
||||||
def fake_create_workers(session, config, count=1, worker_manager=None):
|
|
||||||
"""替换真实 worker 启动,避免依赖 zenoh 会话。与原始实现同为同步函数。"""
|
|
||||||
return []
|
|
||||||
|
|
||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
# 替换 Plugin 基类的 worker 创建实现,避免依赖 zenoh 会话
|
|
||||||
auth_plugin.plugin._create_workers_impl = fake_create_workers
|
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory() as tmp:
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
config = {"type": "auth", "data_dir": tmp}
|
db = await open_database({"data_dir": tmp})
|
||||||
|
|
||||||
# 模拟 4 个 gunicorn worker 进程同时启动
|
# 模拟 4 个 gunicorn worker 进程同时启动
|
||||||
results = await asyncio.gather(
|
results = await asyncio.gather(
|
||||||
auth_plugin.create_workers(None, config, 3),
|
*[seed_admin(db) for _ in range(4)],
|
||||||
auth_plugin.create_workers(None, config, 3),
|
|
||||||
auth_plugin.create_workers(None, config, 3),
|
|
||||||
auth_plugin.create_workers(None, config, 3),
|
|
||||||
return_exceptions=True,
|
return_exceptions=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -42,15 +30,11 @@ async def main():
|
|||||||
raise RuntimeError("并发启动存在未捕获异常")
|
raise RuntimeError("并发启动存在未捕获异常")
|
||||||
|
|
||||||
# 校验 users 表中应只有一条 admin
|
# 校验 users 表中应只有一条 admin
|
||||||
from chongming_database import Database
|
from chongming_auth import Users
|
||||||
from plugins.auth.models.auth import Users
|
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
|
|
||||||
db = Database(Path(tmp) / "auth.db")
|
|
||||||
await db.initialize()
|
|
||||||
async with db.get_session() as sess:
|
async with db.get_session() as sess:
|
||||||
res = await sess.execute(select(Users))
|
rows = (await sess.execute(select(Users))).scalars().all()
|
||||||
rows = res.scalars().all()
|
|
||||||
|
|
||||||
print("✅ 4 个并发启动全部成功")
|
print("✅ 4 个并发启动全部成功")
|
||||||
print(f"✅ users 表记录数: {len(rows)}(应为 1)")
|
print(f"✅ users 表记录数: {len(rows)}(应为 1)")
|
||||||
@@ -58,6 +42,8 @@ async def main():
|
|||||||
print(f" - {u.username} <{u.email}> superuser={u.is_superuser}")
|
print(f" - {u.username} <{u.email}> superuser={u.is_superuser}")
|
||||||
|
|
||||||
assert len(rows) == 1, f"预期 1 条 admin,实际 {len(rows)} 条"
|
assert len(rows) == 1, f"预期 1 条 admin,实际 {len(rows)} 条"
|
||||||
|
assert sum(1 for r in results if r is True) == 1, "应恰好一个并发方完成新建"
|
||||||
|
await db.dispose()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -429,22 +429,39 @@ wheels = [
|
|||||||
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/cc/61/d01fc49b8dea277640b55a9e15960dbca9fdc8c9fde18e572d39c59f4019/charset_normalizer-3.5.1-py3-none-any.whl", hash = "sha256:6df0ec430f9a831772c23ca5a224cba36517a58a84bb32c32bb59a9fa67c47f6", size = 68658, upload-time = "2026-08-15T08:20:43.306Z" },
|
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/cc/61/d01fc49b8dea277640b55a9e15960dbca9fdc8c9fde18e572d39c59f4019/charset_normalizer-3.5.1-py3-none-any.whl", hash = "sha256:6df0ec430f9a831772c23ca5a224cba36517a58a84bb32c32bb59a9fa67c47f6", size = 68658, upload-time = "2026-08-15T08:20:43.306Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "chongming-auth"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = { registry = "http://124.71.68.6:3000/api/packages/chongming/pypi/simple/" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "chongming-database" },
|
||||||
|
{ name = "chongming-jwt" },
|
||||||
|
{ name = "chongming-password" },
|
||||||
|
{ name = "orjson" },
|
||||||
|
{ name = "pydantic" },
|
||||||
|
{ name = "sqlmodel" },
|
||||||
|
]
|
||||||
|
sdist = { url = "http://124.71.68.6:3000/api/packages/chongming/pypi/files/chongming-auth/0.1.0/chongming_auth-0.1.0.tar.gz", hash = "sha256:745aa621e9ad4ed3ca583c2d310362869dce63e9731f5581d9b777b55203487d" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "http://124.71.68.6:3000/api/packages/chongming/pypi/files/chongming-auth/0.1.0/chongming_auth-0.1.0-py3-none-any.whl", hash = "sha256:a89948aa9a0229a79e0f1e2285f1ecf60cefc0c029a7fbdabf0b244a9b902d9a" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "chongming-cache"
|
name = "chongming-cache"
|
||||||
version = "0.2.1"
|
version = "0.2.2"
|
||||||
source = { registry = "http://124.71.68.6:3000/api/packages/chongming/pypi/simple/" }
|
source = { registry = "http://124.71.68.6:3000/api/packages/chongming/pypi/simple/" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "orjson" },
|
{ name = "orjson" },
|
||||||
{ name = "redis" },
|
{ name = "redis" },
|
||||||
]
|
]
|
||||||
sdist = { url = "http://124.71.68.6:3000/api/packages/chongming/pypi/files/chongming-cache/0.2.1/chongming_cache-0.2.1.tar.gz", hash = "sha256:4945a21b6a53c38a263955fddae85ca8036e3ba62e69b117fcab5fd7cb38cd22" }
|
sdist = { url = "http://124.71.68.6:3000/api/packages/chongming/pypi/files/chongming-cache/0.2.2/chongming_cache-0.2.2.tar.gz", hash = "sha256:1c224329747d99171956ae8e4606a08847acfcbfc6664662bca2513f0c0ca1d1" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "http://124.71.68.6:3000/api/packages/chongming/pypi/files/chongming-cache/0.2.1/chongming_cache-0.2.1-py3-none-any.whl", hash = "sha256:1ca873e4c069ff24b728bb1b90342d66cb6fa6dd994851a1d0cddf2604f0693d" },
|
{ url = "http://124.71.68.6:3000/api/packages/chongming/pypi/files/chongming-cache/0.2.2/chongming_cache-0.2.2-py3-none-any.whl", hash = "sha256:902877168da16e34204982643f44d1313bad5a43bca65d348522523d307f0c16" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "chongming-cli"
|
name = "chongming-cli"
|
||||||
version = "0.3.0"
|
version = "0.3.1"
|
||||||
source = { registry = "http://124.71.68.6:3000/api/packages/chongming/pypi/simple/" }
|
source = { registry = "http://124.71.68.6:3000/api/packages/chongming/pypi/simple/" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "chongming-config" },
|
{ name = "chongming-config" },
|
||||||
@@ -452,9 +469,9 @@ dependencies = [
|
|||||||
{ name = "sqlite-vfs" },
|
{ name = "sqlite-vfs" },
|
||||||
{ name = "tomli" },
|
{ name = "tomli" },
|
||||||
]
|
]
|
||||||
sdist = { url = "http://124.71.68.6:3000/api/packages/chongming/pypi/files/chongming-cli/0.3.0/chongming_cli-0.3.0.tar.gz", hash = "sha256:bdd61eb28909587c2b0c8f1cb675606512a242533eeab30b111678868a36c7a0" }
|
sdist = { url = "http://124.71.68.6:3000/api/packages/chongming/pypi/files/chongming-cli/0.3.1/chongming_cli-0.3.1.tar.gz", hash = "sha256:b5925b9b22ffe374720a48d2614a3a06ace020cba53a20e2b24d53c0e1d2305e" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "http://124.71.68.6:3000/api/packages/chongming/pypi/files/chongming-cli/0.3.0/chongming_cli-0.3.0-py3-none-any.whl", hash = "sha256:b8422c1e648828be157002ba816b9b41c496eb680b3b9b1f00129c92e96fb7b5" },
|
{ url = "http://124.71.68.6:3000/api/packages/chongming/pypi/files/chongming-cli/0.3.1/chongming_cli-0.3.1-py3-none-any.whl", hash = "sha256:189c9e43d0c88817844f689e32686c2f4b66f4e25e89ab23c1adb135d7a3f4e2" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -491,6 +508,7 @@ source = { registry = "http://124.71.68.6:3000/api/packages/chongming/pypi/simpl
|
|||||||
wheels = [
|
wheels = [
|
||||||
{ url = "http://124.71.68.6:3000/api/packages/chongming/pypi/files/chongming-jwt/0.1.0/chongming_jwt-0.1.0-cp312-cp312-linux_aarch64.whl", hash = "sha256:0512956acaa7a17aaf9708da8d7b3a7dc366ef0226f03550d7b1b06940ba733b" },
|
{ url = "http://124.71.68.6:3000/api/packages/chongming/pypi/files/chongming-jwt/0.1.0/chongming_jwt-0.1.0-cp312-cp312-linux_aarch64.whl", hash = "sha256:0512956acaa7a17aaf9708da8d7b3a7dc366ef0226f03550d7b1b06940ba733b" },
|
||||||
{ url = "http://124.71.68.6:3000/api/packages/chongming/pypi/files/chongming-jwt/0.1.0/chongming_jwt-0.1.0-cp312-cp312-linux_x86_64.whl", hash = "sha256:527676e710d038f270093d0964c92fc5e976f1f5db23e846e3819b1c8a566721" },
|
{ url = "http://124.71.68.6:3000/api/packages/chongming/pypi/files/chongming-jwt/0.1.0/chongming_jwt-0.1.0-cp312-cp312-linux_x86_64.whl", hash = "sha256:527676e710d038f270093d0964c92fc5e976f1f5db23e846e3819b1c8a566721" },
|
||||||
|
{ url = "http://124.71.68.6:3000/api/packages/chongming/pypi/files/chongming-jwt/0.1.0/chongming_jwt-0.1.0-cp312-cp312-manylinux_2_31_aarch64.whl", hash = "sha256:ec47b7698e928141c9828200bed4377754c296e688f29d694f9b1cc3d5f00bf0" },
|
||||||
{ url = "http://124.71.68.6:3000/api/packages/chongming/pypi/files/chongming-jwt/0.1.0/chongming_jwt-0.1.0-cp312-cp312-manylinux_2_34_aarch64.whl", hash = "sha256:d2329de197af17141048da32b52dcda87ff578353b4fae78c3bbd0043244cdfc" },
|
{ url = "http://124.71.68.6:3000/api/packages/chongming/pypi/files/chongming-jwt/0.1.0/chongming_jwt-0.1.0-cp312-cp312-manylinux_2_34_aarch64.whl", hash = "sha256:d2329de197af17141048da32b52dcda87ff578353b4fae78c3bbd0043244cdfc" },
|
||||||
{ url = "http://124.71.68.6:3000/api/packages/chongming/pypi/files/chongming-jwt/0.1.0/chongming_jwt-0.1.0-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:d63cc24550b7084e55d4dd0cb9f99cbd0a9d77da39ad0685698fd484e4376e72" },
|
{ url = "http://124.71.68.6:3000/api/packages/chongming/pypi/files/chongming-jwt/0.1.0/chongming_jwt-0.1.0-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:d63cc24550b7084e55d4dd0cb9f99cbd0a9d77da39ad0685698fd484e4376e72" },
|
||||||
]
|
]
|
||||||
@@ -514,6 +532,7 @@ source = { registry = "http://124.71.68.6:3000/api/packages/chongming/pypi/simpl
|
|||||||
wheels = [
|
wheels = [
|
||||||
{ url = "http://124.71.68.6:3000/api/packages/chongming/pypi/files/chongming-password/0.1.0/chongming_password-0.1.0-cp312-cp312-linux_aarch64.whl", hash = "sha256:752651cce38a999466e9b342f18f0ceb78b17907646440d2b1c6306ebc9c7648" },
|
{ url = "http://124.71.68.6:3000/api/packages/chongming/pypi/files/chongming-password/0.1.0/chongming_password-0.1.0-cp312-cp312-linux_aarch64.whl", hash = "sha256:752651cce38a999466e9b342f18f0ceb78b17907646440d2b1c6306ebc9c7648" },
|
||||||
{ url = "http://124.71.68.6:3000/api/packages/chongming/pypi/files/chongming-password/0.1.0/chongming_password-0.1.0-cp312-cp312-linux_x86_64.whl", hash = "sha256:97414ddb764a8efa0183eb6a4519530a8522729ff633a06285a88b7b22fa77b4" },
|
{ url = "http://124.71.68.6:3000/api/packages/chongming/pypi/files/chongming-password/0.1.0/chongming_password-0.1.0-cp312-cp312-linux_x86_64.whl", hash = "sha256:97414ddb764a8efa0183eb6a4519530a8522729ff633a06285a88b7b22fa77b4" },
|
||||||
|
{ url = "http://124.71.68.6:3000/api/packages/chongming/pypi/files/chongming-password/0.1.0/chongming_password-0.1.0-cp312-cp312-manylinux_2_31_aarch64.whl", hash = "sha256:22109f4d1d1263bf840554601da9eb2206fb56ba7deae6828f4ebf710edb8dd1" },
|
||||||
{ url = "http://124.71.68.6:3000/api/packages/chongming/pypi/files/chongming-password/0.1.0/chongming_password-0.1.0-cp312-cp312-manylinux_2_34_aarch64.whl", hash = "sha256:65fbe21f0d9cd152e0142b129e23b1697234f5eeba6a9d0ded1a14949849cca3" },
|
{ url = "http://124.71.68.6:3000/api/packages/chongming/pypi/files/chongming-password/0.1.0/chongming_password-0.1.0-cp312-cp312-manylinux_2_34_aarch64.whl", hash = "sha256:65fbe21f0d9cd152e0142b129e23b1697234f5eeba6a9d0ded1a14949849cca3" },
|
||||||
{ url = "http://124.71.68.6:3000/api/packages/chongming/pypi/files/chongming-password/0.1.0/chongming_password-0.1.0-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:9620458d04e1cc9c93842f7b960b2b7af8b9bc5f88643e785d12686301071b3c" },
|
{ url = "http://124.71.68.6:3000/api/packages/chongming/pypi/files/chongming-password/0.1.0/chongming_password-0.1.0-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:9620458d04e1cc9c93842f7b960b2b7af8b9bc5f88643e785d12686301071b3c" },
|
||||||
]
|
]
|
||||||
@@ -565,6 +584,7 @@ dependencies = [
|
|||||||
{ name = "aiofiles" },
|
{ name = "aiofiles" },
|
||||||
{ name = "amqtt" },
|
{ name = "amqtt" },
|
||||||
{ name = "bytedance" },
|
{ name = "bytedance" },
|
||||||
|
{ name = "chongming-auth" },
|
||||||
{ name = "chongming-cache" },
|
{ name = "chongming-cache" },
|
||||||
{ name = "chongming-jwt" },
|
{ name = "chongming-jwt" },
|
||||||
{ name = "chongming-mqtt" },
|
{ name = "chongming-mqtt" },
|
||||||
@@ -601,6 +621,7 @@ requires-dist = [
|
|||||||
{ name = "aiofiles" },
|
{ name = "aiofiles" },
|
||||||
{ name = "amqtt" },
|
{ name = "amqtt" },
|
||||||
{ name = "bytedance" },
|
{ name = "bytedance" },
|
||||||
|
{ name = "chongming-auth" },
|
||||||
{ name = "chongming-cache" },
|
{ name = "chongming-cache" },
|
||||||
{ name = "chongming-jwt" },
|
{ name = "chongming-jwt" },
|
||||||
{ name = "chongming-mqtt" },
|
{ name = "chongming-mqtt" },
|
||||||
|
|||||||
Reference in New Issue
Block a user