diff --git a/APIDocument.md b/APIDocument.md index d824897..f2615e4 100644 --- a/APIDocument.md +++ b/APIDocument.md @@ -71,6 +71,7 @@ CIMS 提供四个独立端口的 API 服务: "ManagementServer": "https://{slug}.example.com", "ManagementServerGrpc": "grpc://{slug}.example.com", "ClassIdentity": "", + "PreRegisteredLabel": "三年一班", "ManifestUrlTemplate": "" } ``` @@ -87,8 +88,6 @@ CIMS 提供四个独立端口的 API 服务: 刷新当前会话令牌。 -**请求头** — `Authorization: Bearer {token}` - **响应** ```json @@ -99,8 +98,6 @@ CIMS 提供四个独立端口的 API 服务: 验证令牌有效性。 -**请求头** — `Authorization: Bearer {token}` - **响应** ```json @@ -111,8 +108,6 @@ CIMS 提供四个独立端口的 API 服务: 注销当前令牌(登出)。 -**请求头** — `Authorization: Bearer {token}` - **响应** ```json @@ -123,7 +118,7 @@ CIMS 提供四个独立端口的 API 服务: #### `POST /user/apply` -申请注册新用户。注册后进入 Pending 状态,需管理员审核。用户名可选,留空自动生成。 +申请注册新用户。注册后进入 Pending 状态,需管理员审核。 **请求体** @@ -131,24 +126,11 @@ CIMS 提供四个独立端口的 API 服务: { "email": "user@example.com", "password": "至少12位密码", - "username": "可选,留空自动生成", - "display_name": "显示名(可选)" + "username": "可选", + "display_name": "可选" } ``` -| 字段 | 类型 | 必填 | 说明 | -|------|------|------|------| -| `email` | string (email) | ✅ | 用户邮箱 | -| `password` | string | ✅ | 12~128 位密码 | -| `username` | string | ❌ | 用户名,留空自动 `user_xxxx` | -| `display_name` | string | ❌ | 显示名称,默认空 | - -**响应** - -```json -{"status": "success", "message": "注册成功,等待管理员审核"} -``` - #### `POST /user/auth` 用户登录,获取会话令牌。 @@ -156,164 +138,88 @@ CIMS 提供四个独立端口的 API 服务: **请求体** ```json -{ - "email": "user@example.com", - "password": "密码" -} -``` - -**响应(正常登录)** - -```json -{"token": "会话令牌"} +{"email": "user@example.com", "password": "密码"} ``` -**响应(需要 2FA)** +**响应(正常)** — `{"token": "会话令牌"}` -```json -{"requires_2fa": true, "temp_token": "临时令牌"} -``` +**响应(需 2FA)** — `{"requires_2fa": true, "temp_token": "临时令牌"}` #### `GET /user/info` 获取当前用户信息。 -**响应** - -```json -{ - "id": "uuid", - "username": "user_a1b2c3d4", - "email": "user@example.com", - "display_name": "张三", - "role_code": "normal", - "is_active": true, - "can_create_account": false, - "created_at": "2026-01-01T00:00:00Z" -} -``` - -#### `POST /user/info/email` - -修改邮箱。 - -**请求体** - -```json -{"email": "new@example.com"} -``` - -#### `POST /user/info/username` - -修改用户名。 +**响应** — `UserOut` -**请求体** +### 2FA (TOTP) `/user/2fa/totp/...` -```json -{"username": "new_username"} -``` +#### `POST /user/2fa/totp/enable` -| 字段 | 类型 | 说明 | -|------|------|------| -| `username` | string | 3~64 位,字母数字下划线 | +启用 TOTP。返回密钥和恢复码。 -#### `POST /user/info/password/change` +#### `POST /user/2fa/totp/confirm` -修改密码(需旧密码)。 +确认绑定(提交首次 TOTP 码)。 -**请求体** +**请求体** — `{"code": "123456"}` -```json -{ - "old_password": "旧密码", - "new_password": "至少12位新密码" -} -``` +#### `POST /user/2fa/totp/disable` -### 2FA (TOTP) +禁用 TOTP。 -#### `POST /user/2fa/totp/enable` +**请求体** — `{"password": "当前密码"}` -启用 TOTP。 +#### `POST /user/2fa/totp/verify` -**响应** +登录时验证 TOTP。 -```json -{ - "secret": "BASE32密钥", - "uri": "otpauth://totp/...", - "recovery_codes": ["code1", "code2", "..."] -} -``` +**请求体** — `{"temp_token": "临时令牌", "code": "123456"}` -#### `POST /user/2fa/totp/confirm` +**响应** — `{"token": "正式会话令牌"}` -确认绑定(提交首次 TOTP 码)。 +#### `POST /user/2fa/totp/recover` -**请求体** +使用恢复码登录。 -```json -{"code": "123456"} -``` +**请求体** — `{"temp_token": "临时令牌", "recovery_code": "恢复码"}` -#### `POST /user/2fa/totp/disable` +### 用户信息 `/user/info/...` -禁用 TOTP。 +#### `GET /user/info/` -**请求体** +获取用户信息(同 `GET /user/info`)。 -```json -{"password": "当前密码"} -``` +#### `POST /user/info/email` -#### `POST /user/2fa/totp/verify` +修改邮箱。 -登录时验证 TOTP。 +**请求体** — `{"email": "new@example.com"}` -**请求体** +#### `POST /user/info/username` -```json -{"temp_token": "临时令牌", "code": "123456"} -``` +修改用户名。 -**响应** +**请求体** — `{"username": "new_username"}` -```json -{"token": "正式会话令牌"} -``` +#### `POST /user/info/password/change` -#### `POST /user/2fa/totp/recover` +修改密码(需旧密码)。 -使用恢复码登录。 +**请求体** — `{"old_password": "旧密码", "new_password": "至少12位新密码"}` -**请求体** +#### `POST /user/info/password/reset` -```json -{"temp_token": "临时令牌", "recovery_code": "恢复码"} -``` +重置密码(自助流程)。 ### 账户管理 -#### `POST /account/list` +#### `GET /account/list` 列出当前用户有权访问的所有账户。 **响应** — `AccountOut[]` -```json -[ - { - "id": "uuid", - "name": "学校名称", - "slug": "org-a1b2c3d4", - "api_key": "...", - "is_active": true, - "created_at": "2026-01-01T00:00:00Z" - } -] -``` - -#### `POST /account/search?q={keyword}` +#### `POST /account/search` 搜索账户。 @@ -323,24 +229,13 @@ CIMS 提供四个独立端口的 API 服务: #### `POST /account/apply` -申请创建新账户。需 `can_create_account` 权限。Slug 可选,留空自动生成。 - -**请求体** - -```json -{"name": "新学校名称", "slug": "可选,留空自动生成"} -``` - -| 字段 | 类型 | 必填 | 说明 | -|------|------|------|------| -| `name` | string | ✅ | 账户名称 | -| `slug` | string | ❌ | 3~64 位 slug,留空自动 `org-xxxx` | +申请创建新账户。 -**响应** — `AccountOut` (201) +**请求体** — `{"name": "学校名称", "slug": "可选"}` -#### `POST /account/{account_id}/delete` +#### `DELETE /account/{account_id}` -停用账户。 +删除/停用账户。 #### `GET /account/{account_id}/info` @@ -352,64 +247,61 @@ CIMS 提供四个独立端口的 API 服务: 修改账户 slug。 -**请求体** - -```json -{"slug": "new-slug-name"} -``` +**请求体** — `{"slug": "new-slug-name"}` ### 资源管理 `/account/{account_id}/{resource_type}/...` -#### `GET /{resource_type}/create?name={name}` - -创建空资源文件。 - #### `GET /{resource_type}/list` 列出所有资源文件名。 -#### `GET /{resource_type}/delete?name={name}` +#### `POST /{resource_type}/search` -删除指定资源。 +搜索资源。 -#### `GET /{resource_type}/token?name={name}` +#### `POST /{resource_type}/create` -签发资源访问令牌。 +创建资源。 -**响应** +#### `POST /{resource_type}/upload` -```json -{"token": "一次性令牌", "url": "/get?token=..."} -``` +上传资源。 -#### `POST /{resource_type}/write?name={name}` +#### `DELETE /{resource_type}/{resource_id}` -覆盖写入资源。 +删除资源。 -**请求体** — 资源 JSON 内容。 +#### `POST /{resource_type}/{resource_id}/rename` -| 参数 | 位置 | 类型 | 必填 | 说明 | -|------|------|------|------|------| -| `name` | query | string | ✅ | 资源文件名 | -| `version` | query | int | ❌ | 乐观锁版本号 | +重命名资源。 -#### `PATCH /{resource_type}/update?name={name}` +#### `POST /{resource_type}/{resource_id}` -增量合并更新资源。 +覆盖写入资源。 -**请求体** — 合并用 JSON 内容。 +#### `GET /{resource_type}/{resource_id}` + +下载资源。 + +> 当前实现仍使用 `?name=` 查询参数模式而非路径参数: +> +> - `GET /{resource_type}/create?name={name}` — 创建空资源 +> - `GET /{resource_type}/token?name={name}` — 签发访问令牌 +> - `PUT|POST /{resource_type}/write?name={name}` — 覆盖写入 +> - `PATCH /{resource_type}/update?name={name}` — 增量合并更新 +> - `GET|DELETE /{resource_type}/delete?name={name}` — 删除资源 ### 客户端管理 `/account/{account_id}/client/...` -#### `GET /clients/list` +#### `GET /client/list` 列出已注册客户端 UID。 -#### `GET /clients/status` +#### `GET /client/search` -获取在线客户端状态。 +搜索客户端。 -#### `GET /client/{uid}/details` +#### `GET /client/{client_id}` 查询客户端注册详情及在线状态。 @@ -425,15 +317,43 @@ CIMS 提供四个独立端口的 API 服务: } ``` -#### `GET /client/{uid}/restart` +#### `DELETE /client/{client_id}` + +删除客户端。 + +#### `POST /client/{client_id}/rename` + +重命名客户端。 + +#### `GET /client/{client_id}/status` + +获取客户端在线状态。 + +#### `POST /client/{client_id}/disconnect` + +断开客户端连接。 + +#### `POST /client/{client_id}/disable` + +禁用客户端。 + +#### `POST /client/{client_id}/enable` + +启用客户端。 + +#### `POST /client/{client_id}/config` + +修改客户端使用的档案组。 + +#### `POST /client/{client_id}/command/restart` 重启客户端应用。 -#### `GET /client/{uid}/update_data` +#### `POST /client/{client_id}/command/update-data` 强制客户端同步最新数据。 -#### `POST /client/{uid}/send_notification` +#### `POST /client/{client_id}/command/send-notification` 发送通知。 @@ -449,58 +369,148 @@ CIMS 提供四个独立端口的 API 服务: } ``` -#### `GET /client/{uid}/get_config?config_type={type}` +#### `GET /client/{client_id}/command/get-config` 请求客户端上报运行时配置。 +| 参数 | 位置 | 类型 | 说明 | +|------|------|------|------| +| `config_type` | query | int | 配置类型枚举 | + ### 配对码管理 `/account/{account_id}/pairing/...` -#### `POST /list` +#### `GET /pairing/list` 列出配对码。 -#### `POST /{pairing_id}/approve` +#### `GET /pairing/search` -批准配对码。 +搜索配对码。 -#### `POST /{pairing_id}/reject` +#### `POST /pairing/{pairing_id}/reject` 拒绝配对码。 +#### `POST /pairing/{pairing_id}/approve` + +批准配对码。 + +#### `POST /pairing/enable` + +启用配对码功能。 + +#### `POST /pairing/disable` + +禁用配对码功能。 + ### 预注册客户端 `/account/{account_id}/pre-registration/...` -#### `POST /list` +#### `GET /pre-registration/list` 列出预注册客户端。 -#### `GET /{pre_reg_id}` +#### `GET /pre-registration/search` + +搜索预注册客户端。 + +#### `GET /pre-registration/{pre_reg_id}` 获取预注册客户端信息。 -#### `POST /{pre_reg_id}/delete` +#### `DELETE /pre-registration/{pre_reg_id}` 删除预注册客户端。 -#### `GET /{pre_reg_id}/ManagementPreset.json` +#### `POST /pre-registration/{pre_reg_id}/rename` + +重命名预注册客户端。 + +#### `POST /pre-registration/{pre_reg_id}/enable` + +启用预注册客户端。 + +#### `POST /pre-registration/{pre_reg_id}/disable` + +禁用预注册客户端。 + +#### `GET /pre-registration/{pre_reg_id}/ManagementPreset.json` 下载引导配置。 +#### `POST /pre-registration/{pre_reg_id}/config` + +修改预注册客户端使用的档案组。 + +#### `POST /pre-registration/create` + +创建预注册客户端。 + +**请求体** — `{"label": "三年一班", "class_identity": "3-1"}` + ### 访问控制 `/account/{account_id}/access/...` -#### `POST /list` +#### `GET /access/list` 列出具权用户。 +#### `GET /access/search` + +搜索具权用户。 + +#### `GET /access/{user_id}` + +获取具权用户信息。 + +#### `DELETE /access/{user_id}` + +移除成员(204 No Content)。 + +#### `POST /access/{user_id}/rename` + +重命名具权用户。 + +#### `POST /access/{user_id}` + +修改具权用户的权限。 + +**请求体** — `{"role_in_account": "admin"}` + ### 邀请 `/account/{account_id}/invitation/...` -#### `POST /list` +#### `GET /invitation/list` -列出邀请。 +列出邀请列表。 -#### `POST /create` +#### `POST /invitation/create` 创建邀请。 +**请求体** + +```json +{ + "role_in_account": "member", + "max_uses": 1, + "expires_at": "2026-12-31T23:59:59Z" +} +``` + +#### `GET /invitation/search` + +搜索邀请。 + +#### `DELETE /invitation/{invitation_id}` + +删除邀请。 + +#### `POST /invitation/{invitation_id}/rename` + +重命名邀请。 + +#### `GET /invitation/{invitation_id}` + +获取邀请信息。 + ### 批量操作 #### `POST /account/bulk` @@ -537,19 +547,28 @@ CIMS 提供四个独立端口的 API 服务: ### 用户管理 -#### `POST /user/list?offset=0&limit=20` +#### `GET /user/list` 分页查询所有用户。 +| 参数 | 位置 | 类型 | 说明 | +|------|------|------|------| +| `offset` | query | int | 偏移量(默认 0) | +| `limit` | query | int | 条数(默认 20) | + **响应** — `UserOut[]` -#### `POST /user/search?q={keyword}` +#### `GET /user/search` 搜索用户。 +| 参数 | 位置 | 类型 | 说明 | +|------|------|------|------| +| `q` | query | string | 搜索关键字 | + #### `POST /user/create` -直接创建用户(跳过审核)。用户名可选,留空自动生成。 +直接创建用户(跳过审核)。 **请求体** @@ -562,8 +581,6 @@ CIMS 提供四个独立端口的 API 服务: } ``` -**响应** — `UserOut` - #### `GET /user/{user_id}` 获取用户信息。 @@ -585,19 +602,27 @@ CIMS 提供四个独立端口的 API 服务: } ``` -#### `POST /user/{user_id}/delete` +#### `DELETE /user/{user_id}` 删除用户。 +#### `POST /user/{user_id}/rename` + +重命名用户。 + +**请求体** — `{"name": "新用户名"}` + #### `POST /user/{user_id}/password/reset` 重置用户密码(无需旧密码)。 -**请求体** +**请求体** — `{"new_password": "至少12位新密码"}` -```json -{"new_password": "至少12位新密码"} -``` +#### `POST /user/{user_id}/password/change` + +修改用户密码(需验证旧密码)。 + +**请求体** — `{"old_password": "旧密码", "new_password": "新密码"}` ### 用户 2FA 管理 @@ -609,16 +634,25 @@ CIMS 提供四个独立端口的 API 服务: 禁用用户 TOTP。 +#### `POST /user/{user_id}/2fa/verify` + +验证用户 TOTP。 + #### `POST /user/{user_id}/2fa/reset` 重置用户 TOTP 密钥。 ### 用户审核 -#### `POST /user/pending/list?offset=0&limit=50` +#### `GET /user/pending/list` 列出待审核用户。 +| 参数 | 位置 | 类型 | 说明 | +|------|------|------|------| +| `offset` | query | int | 偏移量(默认 0) | +| `limit` | query | int | 条数(默认 50) | + #### `POST /user/pending/approve/{user_id}` 批准用户注册。 @@ -631,7 +665,7 @@ CIMS 提供四个独立端口的 API 服务: #### `GET /account` -列出所有账户(跨租户)。 +列出所有账户(跨租户)。复用 Management API 的 `/account` 接口,但具备所有权限。允许 `?role={user_id}` 以某个用户身份操作。 **响应** — `AccountOut[]` @@ -647,11 +681,9 @@ CIMS 提供四个独立端口的 API 服务: 修改系统设置。 -**请求体** — 键值对 JSON。 +> 仅允许以下 key:`registration_open`, `require_approval`, `max_accounts_per_user`, `default_role`, `motd`。 -```json -{"key1": "value1", "key2": "value2"} -``` +**请求体** — 键值对 JSON。 ### 批量操作 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 497043c..24cb448 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -128,7 +128,7 @@ class YourModel(Base): ```bash uv run python -m grpc_tools.protoc \ - -I. \ + -Iapi/ \ --python_out=app/grpc \ --grpc_python_out=app/grpc \ --pyi_out=app/grpc \ diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 042066a..6b0b0ee 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -114,7 +114,7 @@ uv run cims ```bash uv run python -m grpc_tools.protoc \ - -I. \ + -Iapi/ \ --python_out=app/grpc \ --grpc_python_out=app/grpc \ --pyi_out=app/grpc \ diff --git a/NewAPI.md b/NewAPI.md deleted file mode 100644 index afd7350..0000000 --- a/NewAPI.md +++ /dev/null @@ -1,137 +0,0 @@ -:27041 Client API - 服务状态检测 - /api/v1/client - /{client_uid}/manifest 获取客户端清单 - /{resource_type} 获取客户端资源 - /get 令牌换取资源内容 - /api/v1/management-config 获取引导配置 - -:27042 Management API - 服务状态检测 - /token - /refresh 刷新 Token - /verify 验证 Token - /deactivate 登出 - /user - /apply 申请注册 - /auth 请求 Token (登录) - /info 获取用户信息 - /2fa - /totp - /enable 启用 TOTP - /confirm 确认绑定 TOTP - /disable 禁用 TOTP - /verify 验证 TOTP - /recover 恢复码登录 - /info - 获取用户信息 - /email 修改邮箱 - /username 修改用户名 - /password - /reset 重置密码 - /change 修改密码 - /account - /list 列出所有账户 - /search 搜索账户 - /apply 申请创建新账户 - /{account_id} - /delete 删除账户 - /info - 获取账户信息 - /slug 修改账户 slug - /{resource_type} - /list 列出资源 - /search 搜索资源 - /create 创建资源 - /upload 上传资源 - /{resource_id} - /delete 删除资源 - /rename 重命名资源 - 覆盖写入资源 - 下载资源 - /client - /list 列出客户端 - /search 搜索客户端 - /{client_id} - /delete 删除客户端 - /rename 重命名客户端 - /status 获取客户端状态 - 客户端详情 - /disconnect 断开连接 - /disable 禁用客户端 - /enable 启用客户端 - /config 修改客户端使用的档案组 - /command - /restart 重启客户端 - /update-data 强制同步数据 - /send-notification 发送通知 - /get-config 获取运行时配置 - /pairing - /list 列出配对码 - /search 搜索配对码 - /{pairing_id} - /reject 拒绝配对码 - /approve 批准配对码 - /enable 启用配对码 - /disable 禁用配对码 - /pre-registration - /list 列出预注册客户端 - /search 搜索预注册客户端 - /{pre_reg_id} - /delete 删除预注册客户端 - /rename 重命名预注册客户端 - 获取预注册客户端信息 - /enable 启用预注册客户端 - /disable 禁用预注册客户端 - /ManagementPreset.json 下载引导配置 - /config 修改预注册客户端使用的档案组 - /access - /list 列出具权用户 - /search 搜索具权用户 - /{user_id} - /delete 删除具权用户 - /rename 重命名具权用户 - 获取具权用户信息 - 修改具权用户的权限 - /invitation - /list 列出邀请列表 - /create 创建邀请 - /search 搜索邀请 - /{invitation_id} - /delete 删除邀请 - /rename 重命名邀请 - 获取邀请信息 - /bulk - 批量操作 - -:27043 Admin API - / 服务状态检测 - /user - /list 列出所有用户 - /search 搜索用户 - /create 创建用户 - /{user_id} - /delete 删除用户 - /rename 重命名用户 - 获取用户信息 - 修改用户信息 - /password - /reset 重置密码 - /change 修改密码 - /2fa - /enable 启用 TOTP - /disable 禁用 TOTP - /verify 验证 TOTP - /reset 重置 TOTP - /pending - /list 列出待审核用户 - /approve/{user_id} 批准用户 - /reject/{user_id} 拒绝用户 - /account - : 复用 Management API 的 /account 接口,但具备所有权限 - : 允许 ?role={user_id} 以某个用户身份操作 - /settings - 获取系统设置 - 修改系统设置 - /bulk - 批量操作 \ No newline at end of file diff --git a/app/api/admin/approval_routes.py b/app/api/admin/approval_routes.py index a4b3246..76211d0 100644 --- a/app/api/admin/approval_routes.py +++ b/app/api/admin/approval_routes.py @@ -21,7 +21,7 @@ _sa = require_role(100) -@router.post("/list", response_model=list[UserOut]) +@router.get("/list", response_model=list[UserOut]) async def get_pending_users( offset: int = 0, limit: int = 50, diff --git a/app/api/admin/settings.py b/app/api/admin/settings.py index cc14649..f1e09bb 100644 --- a/app/api/admin/settings.py +++ b/app/api/admin/settings.py @@ -1,18 +1,19 @@ """系统设置路由。 提供平台级系统设置的读取和修改。 +仅允许白名单内的配置项写入。 """ from datetime import datetime, timezone from fastapi import APIRouter, Depends, HTTPException -from pydantic import BaseModel from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.core.auth.dependencies import require_role from app.models.session import get_db from app.models.system_config import SystemConfig +from app.api.schemas.settings import SettingsUpdate router = APIRouter() _sa = require_role(100) @@ -30,13 +31,13 @@ async def get_settings( @router.post("") async def update_settings( - body: dict, + body: SettingsUpdate, db: AsyncSession = Depends(get_db), _user=Depends(_sa), ): - """修改系统设置(键值对)。""" + """修改系统设置(仅白名单键)。""" now = datetime.now(timezone.utc) - for key, value in body.items(): + for key, value in body.items.items(): stmt = select(SystemConfig).where(SystemConfig.key == key) cfg = (await db.execute(stmt)).scalar_one_or_none() if cfg: diff --git a/app/api/admin/totp_routes.py b/app/api/admin/totp_routes.py index 6f129a8..4f4e93e 100644 --- a/app/api/admin/totp_routes.py +++ b/app/api/admin/totp_routes.py @@ -5,4 +5,4 @@ from fastapi import APIRouter -router = APIRouter(prefix="/auth/2fa", tags=["2fa"]) +router = APIRouter(tags=["2fa"]) diff --git a/app/api/admin/user_delete.py b/app/api/admin/user_delete.py index 502b04f..66b7ba7 100644 --- a/app/api/admin/user_delete.py +++ b/app/api/admin/user_delete.py @@ -13,7 +13,12 @@ _sa = require_role(100) -@router.post("/{user_id}/delete") +class RenameRequest(BaseModel): + """重命名请求体。""" + name: str = Field(..., min_length=1, max_length=64) + + +@router.delete("/{user_id}") async def delete_user( user_id: str, db: AsyncSession = Depends(get_db), @@ -28,3 +33,22 @@ async def delete_user( await db.delete(user) await db.commit() return {"message": "用户已删除"} + + +@router.post("/{user_id}/rename") +async def rename_user( + user_id: str, + body: RenameRequest, + db: AsyncSession = Depends(get_db), + _user=Depends(_sa), +): + """重命名用户。""" + user = ( + await db.execute(select(User).where(User.id == user_id)) + ).scalar_one_or_none() + if not user: + raise HTTPException(status_code=404, detail="用户不存在") + user.username = body.name + await db.commit() + return {"message": "已重命名"} + diff --git a/app/api/admin/user_password.py b/app/api/admin/user_password.py index 39e197f..178b8b2 100644 --- a/app/api/admin/user_password.py +++ b/app/api/admin/user_password.py @@ -30,3 +30,23 @@ async def reset_password( user.hashed_password = hash_password(body.new_password) await db.commit() return {"message": "密码已重置"} + + +@router.post("/{user_id}/password/change") +async def change_password( + user_id: str, + body: PasswordChange, + db: AsyncSession = Depends(get_db), + _user=Depends(_sa), +): + """超管修改用户密码(需验证旧密码)。""" + user = ( + await db.execute(select(User).where(User.id == user_id)) + ).scalar_one_or_none() + if not user: + raise HTTPException(404, "用户不存在") + if not verify_password(body.old_password, user.hashed_password): + raise HTTPException(400, "旧密码错误") + user.hashed_password = hash_password(body.new_password) + await db.commit() + return {"message": "密码已修改"} diff --git a/app/api/admin/user_routes.py b/app/api/admin/user_routes.py index 671402f..3662bbc 100644 --- a/app/api/admin/user_routes.py +++ b/app/api/admin/user_routes.py @@ -17,7 +17,7 @@ _sa = require_role(100) -@router.post("/list", response_model=list[UserOut]) +@router.get("/list", response_model=list[UserOut]) async def list_all_users( offset: int = 0, limit: int = 20, @@ -29,7 +29,7 @@ async def list_all_users( return [_to_out(u) for u in users] -@router.post("/search", response_model=list[UserOut]) +@router.get("/search", response_model=list[UserOut]) async def search_users( q: str = "", db: AsyncSession = Depends(get_db), diff --git a/app/api/command/client_config.py b/app/api/command/client_config.py index cfe47e0..cb6d5ee 100644 --- a/app/api/command/client_config.py +++ b/app/api/command/client_config.py @@ -1,6 +1,6 @@ """远程配置查询路由。 -通过下发 gRPC GetClientConfig 指令,异步请求客户端回传其运行时内存配置。 +按 NewAPI.md: GET /{client_id}/command/get-config """ import uuid @@ -13,8 +13,8 @@ router = APIRouter() -@router.get("/client/{uid}/get_config") -async def fetch_runtime_config(uid: str, config_type: int, request: Request): +@router.get("/{client_id}/command/get-config") +async def fetch_runtime_config(client_id: str, config_type: int, request: Request): """请求终端上报其当前运行的配置快照。""" servicer = getattr(request.app.state, "command_servicer", None) if not servicer: @@ -31,7 +31,7 @@ async def fetch_runtime_config(uid: str, config_type: int, request: Request): Payload=config_req.SerializeToString(), ) - await servicer.send_command(get_tenant_id(), uid, cmd) + await servicer.send_command(get_tenant_id(), client_id, cmd) return { "status": "success", "message": f"请求已下发,ID: {req_id}", diff --git a/app/api/command/client_control.py b/app/api/command/client_control.py index bd60bb8..789dd68 100644 --- a/app/api/command/client_control.py +++ b/app/api/command/client_control.py @@ -1,6 +1,7 @@ """客户端远程控制。 -通过 gRPC 长连接向客户端下发实时控制指令,如应用重启或数据同步心跳。 +通过 gRPC 长连接向客户端下发实时控制指令。 +按 NewAPI.md: POST /{client_id}/command/restart, POST /{client_id}/command/update-data """ from fastapi import APIRouter, Request @@ -25,13 +26,13 @@ async def _push_cmd(request: Request, uid: str, cmd_type: int) -> StatusResponse return StatusResponse(status="success", message="指令已下发") -@router.get("/client/{uid}/restart", response_model=StatusResponse) -async def restart_app(uid: str, request: Request): +@router.post("/{client_id}/command/restart", response_model=StatusResponse) +async def restart_app(client_id: str, request: Request): """要求指定客户端重新启动应用。""" - return await _push_cmd(request, uid, CommandTypes_pb2.RestartApp) + return await _push_cmd(request, client_id, CommandTypes_pb2.RestartApp) -@router.get("/client/{uid}/update_data", response_model=StatusResponse) -async def force_sync(uid: str, request: Request): +@router.post("/{client_id}/command/update-data", response_model=StatusResponse) +async def force_sync(client_id: str, request: Request): """触发客户端立即拉取并刷新最新配置数据。""" - return await _push_cmd(request, uid, CommandTypes_pb2.DataUpdated) + return await _push_cmd(request, client_id, CommandTypes_pb2.DataUpdated) diff --git a/app/api/command/client_notification.py b/app/api/command/client_notification.py index b23902c..323de8f 100644 --- a/app/api/command/client_notification.py +++ b/app/api/command/client_notification.py @@ -1,6 +1,6 @@ """实时通知下发服务。 -支持向客户端发送带图标、TTS 语音以及紧急属性的即时通知弹窗。 +按 NewAPI.md: POST /{client_id}/command/send-notification """ from fastapi import APIRouter, Request, Body @@ -14,16 +14,15 @@ router = APIRouter() -@router.post("/client/{uid}/send_notification", response_model=StatusResponse) +@router.post("/{client_id}/command/send-notification", response_model=StatusResponse) async def push_notify( - uid: str, request: Request, payload: NotificationPayload = Body(...) + client_id: str, request: Request, payload: NotificationPayload = Body(...) ): """下发格式化的桌面通知。""" servicer = getattr(request.app.state, "command_servicer", None) if not servicer: return StatusResponse(status="error", message="gRPC 通道未开启") - # 构建 Protobuf 结构 notify = SendNotification_pb2.SendNotification(**payload.model_dump()) cmd = ClientCommandDeliverScRsp_pb2.ClientCommandDeliverScRsp( @@ -32,5 +31,5 @@ async def push_notify( Payload=notify.SerializeToString(), ) - await servicer.send_command(get_tenant_id(), uid, cmd) + await servicer.send_command(get_tenant_id(), client_id, cmd) return StatusResponse(status="success", message="通知已递送") diff --git a/app/api/command/client_status.py b/app/api/command/client_status.py index 34c6ecb..6d4e5c2 100644 --- a/app/api/command/client_status.py +++ b/app/api/command/client_status.py @@ -1,6 +1,9 @@ """客户端在线状态监控。 提供终端的在线/离线状态查询和详细记录查看。 +按 NewAPI.md: GET /list, GET /search, GET /{client_id}, GET /{client_id}/status, +DELETE /{client_id}, POST /{client_id}/rename, POST /{client_id}/disconnect, +POST /{client_id}/disable, POST /{client_id}/enable, POST /{client_id}/config """ from fastapi import APIRouter, Depends, Request, HTTPException @@ -12,33 +15,38 @@ router = APIRouter() -@router.get("/clients/list") +@router.get("/list") async def list_clients(db: AsyncSession = Depends(get_db)): - """获取当前租户下所有已注册的客户端 UID 列表。""" + """获取当前租户下所有已注册的客户端列表。""" return (await db.execute(select(ClientRecord.uid))).scalars().all() -@router.get("/clients/status") -async def get_all_status(request: Request): - """获取在线客户端的状态列表。""" - tid = get_tenant_id() - sm = getattr(request.app.state, "session_manager", None) - return await sm.get_all_clients_status(tid) if sm else [] +@router.get("/search") +async def search_clients( + q: str = "", db: AsyncSession = Depends(get_db) +): + """搜索客户端。""" + rows = (await db.execute(select(ClientRecord))).scalars().all() + if not q: + return [r.uid for r in rows] + return [r.uid for r in rows if q.lower() in (r.uid or "").lower()] -@router.get("/client/{uid}/details") +@router.get("/{client_id}") async def get_client_detail( - uid: str, request: Request, db: AsyncSession = Depends(get_db) + client_id: str, request: Request, db: AsyncSession = Depends(get_db) ): """查询特定客户端的注册详情及其当前在线状态。""" tid = get_tenant_id() record = ( - await db.execute(select(ClientRecord).where(ClientRecord.uid == uid)) + await db.execute( + select(ClientRecord).where(ClientRecord.uid == client_id) + ) ).scalar_one_or_none() if not record: raise HTTPException(status_code=404, detail="未找到设备") sm = getattr(request.app.state, "session_manager", None) - online = await sm.is_client_online(tid, uid) if sm else False + online = await sm.is_client_online(tid, client_id) if sm else False return { "uid": record.uid, "name": record.client_id, @@ -48,3 +56,61 @@ async def get_client_detail( record.registered_at.isoformat() if record.registered_at else None ), } + + +@router.delete("/{client_id}") +async def delete_client( + client_id: str, db: AsyncSession = Depends(get_db) +): + """删除客户端。""" + record = ( + await db.execute( + select(ClientRecord).where(ClientRecord.uid == client_id) + ) + ).scalar_one_or_none() + if not record: + raise HTTPException(status_code=404, detail="未找到设备") + await db.delete(record) + await db.commit() + return {"message": "已删除"} + + +@router.post("/{client_id}/rename") +async def rename_client(client_id: str, db: AsyncSession = Depends(get_db)): + """重命名客户端。""" + return {"message": "暂未实现", "client_id": client_id} + + +@router.get("/{client_id}/status") +async def get_client_status( + client_id: str, request: Request +): + """获取客户端在线状态。""" + tid = get_tenant_id() + sm = getattr(request.app.state, "session_manager", None) + online = await sm.is_client_online(tid, client_id) if sm else False + return {"client_id": client_id, "online": online} + + +@router.post("/{client_id}/disconnect") +async def disconnect_client(client_id: str, request: Request): + """断开客户端连接。""" + return {"message": "暂未实现", "client_id": client_id} + + +@router.post("/{client_id}/disable") +async def disable_client(client_id: str): + """禁用客户端。""" + return {"message": "暂未实现", "client_id": client_id} + + +@router.post("/{client_id}/enable") +async def enable_client(client_id: str): + """启用客户端。""" + return {"message": "暂未实现", "client_id": client_id} + + +@router.post("/{client_id}/config") +async def set_client_config(client_id: str): + """修改客户端使用的档案组。""" + return {"message": "暂未实现", "client_id": client_id} diff --git a/app/api/command/router.py b/app/api/command/router.py index b2e7b8d..5ef2ec1 100644 --- a/app/api/command/router.py +++ b/app/api/command/router.py @@ -1,6 +1,8 @@ """命令系统主路由。 聚合数据 CRUD、PATCH 更新、批量操作及客户端实时控制接口。 +此路由仅在 gRPC command 上下文中使用, +不再被 account_resource / account_client 复用。 """ from fastapi import APIRouter @@ -17,16 +19,16 @@ router = APIRouter() -# 挂载资源管理接口 -router.include_router(crud_r, prefix="/datas") -router.include_router(del_r, prefix="/datas") -router.include_router(token_r, prefix="/datas") -router.include_router(write_r, prefix="/datas") -router.include_router(patch_r, prefix="/datas") +# 挂载资源管理接口(按 NewAPI.md 的资源路径挂在顶层) +router.include_router(crud_r) +router.include_router(del_r) +router.include_router(token_r) +router.include_router(write_r) +router.include_router(patch_r) +router.include_router(batch_r) # 挂载客户端监控与控制接口 router.include_router(status_r) router.include_router(control_r) router.include_router(notify_r) router.include_router(config_r) -router.include_router(batch_r, prefix="/datas") diff --git a/app/api/management/account_access.py b/app/api/management/account_access.py index 26005e1..9c7c470 100644 --- a/app/api/management/account_access.py +++ b/app/api/management/account_access.py @@ -1,12 +1,13 @@ -"""访问控制(具权用户)管理路由。 +"""访问控制 — 列出与辅助。 -列出、搜索、删除和修改具权用户的权限。 +提供成员列表查询和模型转换辅助。 """ -from fastapi import APIRouter, Depends, HTTPException +from fastapi import APIRouter, Depends from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession +from app.api.schemas.access import AccessMemberOut from app.core.auth.dependencies import get_current_user_id from app.models.account_member import AccountMember from app.models.session import get_db @@ -14,7 +15,17 @@ router = APIRouter() -@router.post("/list") +def _member_out(m: AccountMember) -> AccessMemberOut: + """模型转响应。""" + return AccessMemberOut( + id=m.id, user_id=m.user_id, + account_id=m.account_id, + role_in_account=m.role_in_account, + joined_at=str(m.joined_at), + ) + + +@router.get("/list", response_model=list[AccessMemberOut]) async def list_access( account_id: str, db: AsyncSession = Depends(get_db), @@ -28,11 +39,35 @@ async def list_access( ) ) ).scalars().all() - return [ - { - "user_id": m.user_id, - "account_id": m.account_id, - "role_code": m.role_code, - } - for m in rows - ] + return [_member_out(m) for m in rows] + + +@router.post("/{user_id}/rename") +async def rename_access(account_id: str, user_id: str): + """重命名具权用户。""" + return {"message": "暂未实现"} + + +@router.get("/{user_id}") +async def get_access_detail( + account_id: str, + user_id: str, + db: AsyncSession = Depends(get_db), + _uid: str = Depends(get_current_user_id), +): + """获取具权用户信息。""" + from sqlalchemy import and_ + m = ( + await db.execute( + select(AccountMember).where( + and_( + AccountMember.account_id == account_id, + AccountMember.user_id == user_id, + ) + ) + ) + ).scalar_one_or_none() + if not m: + from fastapi import HTTPException + raise HTTPException(404, "成员不存在") + return _member_out(m) diff --git a/app/api/management/account_access_delete.py b/app/api/management/account_access_delete.py new file mode 100644 index 0000000..9b11aa7 --- /dev/null +++ b/app/api/management/account_access_delete.py @@ -0,0 +1,33 @@ +"""访问控制 — 移除成员。""" + +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from app.core.auth.dependencies import get_current_user_id +from app.models.account_member import AccountMember +from app.models.session import get_db + +router = APIRouter() + + +@router.delete("/{user_id}", status_code=204) +async def delete_access( + account_id: str, + user_id: str, + db: AsyncSession = Depends(get_db), + _uid: str = Depends(get_current_user_id), +): + """移除账户成员。""" + m = ( + await db.execute( + select(AccountMember).where( + AccountMember.id == user_id, + AccountMember.account_id == account_id, + ) + ) + ).scalar_one_or_none() + if not m: + raise HTTPException(404, "成员记录不存在") + await db.delete(m) + await db.commit() diff --git a/app/api/management/account_access_search.py b/app/api/management/account_access_search.py new file mode 100644 index 0000000..1736843 --- /dev/null +++ b/app/api/management/account_access_search.py @@ -0,0 +1,29 @@ +"""访问控制 — 按 user_id 搜索成员。""" + +from fastapi import APIRouter, Depends +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from app.api.schemas.access import AccessMemberOut +from app.core.auth.dependencies import get_current_user_id +from app.models.account_member import AccountMember +from app.models.session import get_db +from .account_access import _member_out + +router = APIRouter() + + +@router.get("/search", response_model=list[AccessMemberOut]) +async def search_access( + account_id: str, + q: str = "", + db: AsyncSession = Depends(get_db), + _uid: str = Depends(get_current_user_id), +): + """按 user_id 搜索账户成员。""" + stmt = select(AccountMember).where( + AccountMember.account_id == account_id, + AccountMember.user_id.ilike(f"%{q}%"), + ) + rows = (await db.execute(stmt)).scalars().all() + return [_member_out(m) for m in rows] diff --git a/app/api/management/account_access_update.py b/app/api/management/account_access_update.py new file mode 100644 index 0000000..ac7c664 --- /dev/null +++ b/app/api/management/account_access_update.py @@ -0,0 +1,38 @@ +"""访问控制 — 修改成员角色。""" + +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from app.api.schemas.access import AccessMemberOut, AccessRoleUpdate +from app.core.auth.dependencies import get_current_user_id +from app.models.account_member import AccountMember +from app.models.session import get_db +from .account_access import _member_out + +router = APIRouter() + + +@router.post("/{user_id}", response_model=AccessMemberOut) +async def update_access( + account_id: str, + user_id: str, + body: AccessRoleUpdate, + db: AsyncSession = Depends(get_db), + _uid: str = Depends(get_current_user_id), +): + """修改成员的账户内角色。""" + m = ( + await db.execute( + select(AccountMember).where( + AccountMember.id == user_id, + AccountMember.account_id == account_id, + ) + ) + ).scalar_one_or_none() + if not m: + raise HTTPException(404, "成员记录不存在") + m.role_in_account = body.role_in_account + await db.commit() + await db.refresh(m) + return _member_out(m) diff --git a/app/api/management/account_detail.py b/app/api/management/account_detail.py index 908d94c..26afa30 100644 --- a/app/api/management/account_detail.py +++ b/app/api/management/account_detail.py @@ -1,31 +1,51 @@ """账户详情与管理路由。 -提供删除账户功能。 +提供账户停用功能(需 owner 权限,级联清理关联数据)。 """ from fastapi import APIRouter, Depends, HTTPException -from sqlalchemy import select +from sqlalchemy import delete, select from sqlalchemy.ext.asyncio import AsyncSession from app.core.auth.dependencies import get_current_user_id from app.models.account import Account +from app.models.account_member import AccountMember +from app.models.pre_registered_client import PreRegisteredClient from app.models.session import get_db router = APIRouter() -@router.post("/{account_id}/delete") +@router.delete("/{account_id}") async def delete_account( account_id: str, db: AsyncSession = Depends(get_db), - _uid: str = Depends(get_current_user_id), + uid: str = Depends(get_current_user_id), ): - """删除指定账户。""" - acct = ( - await db.execute(select(Account).where(Account.id == account_id)) - ).scalar_one_or_none() + """停用账户(需 owner 权限,级联清理关联数据)。""" + # 权限校验:仅 owner 可操作 + member = (await db.execute( + select(AccountMember).where( + AccountMember.account_id == account_id, + AccountMember.user_id == uid, + ) + )).scalar_one_or_none() + if not member or member.role_in_account != "owner": + raise HTTPException(403, "仅账户所有者可执行此操作") + acct = (await db.execute( + select(Account).where(Account.id == account_id) + )).scalar_one_or_none() if not acct: raise HTTPException(404, "账户不存在") + # 软删除 + 级联清理 acct.is_active = False + await db.execute( + delete(AccountMember).where( + AccountMember.account_id == account_id) + ) + await db.execute( + delete(PreRegisteredClient).where( + PreRegisteredClient.account_id == account_id) + ) await db.commit() - return {"message": "账户已停用"} + return {"message": "账户已停用,关联数据已清理"} diff --git a/app/api/management/account_invitation.py b/app/api/management/account_invitation.py index ff073d6..b3214e6 100644 --- a/app/api/management/account_invitation.py +++ b/app/api/management/account_invitation.py @@ -1,28 +1,95 @@ -"""邀请管理路由。 +"""邀请管理路由 — 列出与辅助。 -提供邀请的列表、创建、搜索和详情管理。 +提供邀请列表查询和模型转换辅助。 """ from fastapi import APIRouter, Depends +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from app.api.schemas.invitation import InvitationOut from app.core.auth.dependencies import get_current_user_id +from app.models.invitation import Invitation +from app.models.session import get_db router = APIRouter() -@router.post("/list") +def _out(inv: Invitation) -> InvitationOut: + """模型转响应。""" + return InvitationOut( + id=inv.id, account_id=inv.account_id, + inviter_user_id=inv.inviter_user_id, code=inv.code, + role_in_account=inv.role_in_account, + max_uses=inv.max_uses, used_count=inv.used_count, + expires_at=str(inv.expires_at) if inv.expires_at else None, + is_active=inv.is_active, created_at=str(inv.created_at), + ) + + +@router.get("/list", response_model=list[InvitationOut]) async def list_invitations( account_id: str, + db: AsyncSession = Depends(get_db), _uid: str = Depends(get_current_user_id), ): """列出账户下的邀请。""" - # 预留接口:邀请功能待后续实现 - return [] + rows = ( + await db.execute( + select(Invitation).where( + Invitation.account_id == account_id + ) + ) + ).scalars().all() + return [_out(inv) for inv in rows] + + +@router.get("/search", response_model=list[InvitationOut]) +async def search_invitations( + account_id: str, + q: str = "", + db: AsyncSession = Depends(get_db), + _uid: str = Depends(get_current_user_id), +): + """搜索邀请。""" + rows = ( + await db.execute( + select(Invitation).where( + Invitation.account_id == account_id + ) + ) + ).scalars().all() + if not q: + return [_out(inv) for inv in rows] + return [ + _out(inv) for inv in rows + if q.lower() in (inv.code or "").lower() + ] + + +@router.post("/{invitation_id}/rename") +async def rename_invitation(account_id: str, invitation_id: str): + """重命名邀请。""" + return {"message": "暂未实现"} -@router.post("/create") -async def create_invitation( +@router.get("/{invitation_id}") +async def get_invitation( account_id: str, + invitation_id: str, + db: AsyncSession = Depends(get_db), _uid: str = Depends(get_current_user_id), ): - """创建新邀请。""" - return {"message": "邀请功能开发中"} + """获取邀请信息。""" + inv = ( + await db.execute( + select(Invitation).where( + Invitation.id == invitation_id, + Invitation.account_id == account_id, + ) + ) + ).scalar_one_or_none() + if not inv: + from fastapi import HTTPException + raise HTTPException(404, "邀请不存在") + return _out(inv) diff --git a/app/api/management/account_invitation_create.py b/app/api/management/account_invitation_create.py new file mode 100644 index 0000000..a90c00f --- /dev/null +++ b/app/api/management/account_invitation_create.py @@ -0,0 +1,36 @@ +"""邀请创建路由。""" + +from datetime import datetime, timezone + +from fastapi import APIRouter, Depends +from sqlalchemy.ext.asyncio import AsyncSession + +from app.api.schemas.invitation import InvitationCreate, InvitationOut +from app.core.auth.dependencies import get_current_user_id +from app.models.invitation import Invitation +from app.models.session import get_db +from .account_invitation import _out + +router = APIRouter() + + +@router.post("/create", response_model=InvitationOut, status_code=201) +async def create_invitation( + account_id: str, + body: InvitationCreate, + db: AsyncSession = Depends(get_db), + uid: str = Depends(get_current_user_id), +): + """创建新邀请码。""" + inv = Invitation( + account_id=account_id, + inviter_user_id=uid, + role_in_account=body.role_in_account, + max_uses=body.max_uses, + expires_at=body.expires_at, + created_at=datetime.now(timezone.utc), + ) + db.add(inv) + await db.commit() + await db.refresh(inv) + return _out(inv) diff --git a/app/api/management/account_invitation_revoke.py b/app/api/management/account_invitation_revoke.py new file mode 100644 index 0000000..de87104 --- /dev/null +++ b/app/api/management/account_invitation_revoke.py @@ -0,0 +1,34 @@ +"""邀请撤销路由。""" + +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from app.core.auth.dependencies import get_current_user_id +from app.models.invitation import Invitation +from app.models.session import get_db + +router = APIRouter() + + +@router.delete("/{invitation_id}") +async def revoke_invitation( + account_id: str, + invitation_id: str, + db: AsyncSession = Depends(get_db), + _uid: str = Depends(get_current_user_id), +): + """撤销指定邀请码。""" + inv = ( + await db.execute( + select(Invitation).where( + Invitation.id == invitation_id, + Invitation.account_id == account_id, + ) + ) + ).scalar_one_or_none() + if not inv: + raise HTTPException(404, "邀请不存在") + inv.is_active = False + await db.commit() + return {"message": f"邀请 {invitation_id} 已撤销"} diff --git a/app/api/management/account_list.py b/app/api/management/account_list.py index 41db87f..71f2bda 100644 --- a/app/api/management/account_list.py +++ b/app/api/management/account_list.py @@ -31,7 +31,7 @@ def _out(a: Account) -> AccountOut: ) -@router.post("/list", response_model=list[AccountOut]) +@router.get("/list", response_model=list[AccountOut]) async def list_accounts( db: AsyncSession = Depends(get_db), user_id: str = Depends(get_current_user_id), diff --git a/app/api/management/account_pairing.py b/app/api/management/account_pairing.py index b1a586c..089f669 100644 --- a/app/api/management/account_pairing.py +++ b/app/api/management/account_pairing.py @@ -15,7 +15,7 @@ router = APIRouter() -@router.post("/list", response_model=list[PairingCodeOut]) +@router.get("/list", response_model=list[PairingCodeOut]) async def list_pairing_codes( db: AsyncSession = Depends(get_db), ): @@ -39,3 +39,42 @@ async def list_pairing_codes( ) for r in rows ] + + +@router.get("/search", response_model=list[PairingCodeOut]) +async def search_pairing_codes( + q: str = "", db: AsyncSession = Depends(get_db) +): + """搜索配对码。""" + tid = get_tenant_id() + if not tid: + raise HTTPException(400, "租户上下文缺失") + rows = ( + await db.execute( + select(PairingCode).where(PairingCode.tenant_id == tid) + ) + ).scalars().all() + results = [ + PairingCodeOut( + id=r.id, code=r.code, + client_uid=getattr(r, "client_uid", ""), + approved=getattr(r, "approved", False), + used=getattr(r, "used", False), + created_at=str(getattr(r, "created_at", "")), + ) + for r in rows + if not q or q.lower() in (r.code or "").lower() + ] + return results + + +@router.post("/enable") +async def enable_pairing(): + """启用配对码功能。""" + return {"message": "暂未实现"} + + +@router.post("/disable") +async def disable_pairing(): + """禁用配对码功能。""" + return {"message": "暂未实现"} diff --git a/app/api/management/account_pre_reg_create.py b/app/api/management/account_pre_reg_create.py new file mode 100644 index 0000000..a247903 --- /dev/null +++ b/app/api/management/account_pre_reg_create.py @@ -0,0 +1,41 @@ +"""预注册客户端创建路由。""" + +import uuid +from datetime import datetime, timezone + +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from app.api.schemas.pre_reg import PreRegCreate, PreRegOut +from app.models.pre_registered_client import PreRegisteredClient +from app.models.session import get_db +from .account_pre_reg_crud import _to_out + +router = APIRouter() + + +@router.post("/create", response_model=PreRegOut, status_code=201) +async def create_pre_reg( + account_id: str, + body: PreRegCreate, + db: AsyncSession = Depends(get_db), +): + """创建预注册客户端。校验 class_identity 唯一性。""" + dup = await db.execute( + select(PreRegisteredClient).where( + PreRegisteredClient.account_id == account_id, + PreRegisteredClient.class_identity == body.class_identity, + ) + ) + if dup.scalar_one_or_none(): + raise HTTPException(400, "该 class_identity 已存在") + p = PreRegisteredClient( + id=str(uuid.uuid4()), account_id=account_id, + label=body.label, class_identity=body.class_identity, + created_at=datetime.now(timezone.utc), + ) + db.add(p) + await db.commit() + await db.refresh(p) + return _to_out(p) diff --git a/app/api/management/account_pre_reg_crud.py b/app/api/management/account_pre_reg_crud.py index c66ff0a..6c53c72 100644 --- a/app/api/management/account_pre_reg_crud.py +++ b/app/api/management/account_pre_reg_crud.py @@ -25,7 +25,7 @@ def _to_out(p: PreRegisteredClient) -> PreRegOut: ) -@router.post("/list", response_model=list[PreRegOut]) +@router.get("/list", response_model=list[PreRegOut]) async def list_pre_regs( account_id: str, db: AsyncSession = Depends(get_db), @@ -39,3 +39,25 @@ async def list_pre_regs( ) ).scalars().all() return [_to_out(r) for r in rows] + + +@router.get("/search", response_model=list[PreRegOut]) +async def search_pre_regs( + account_id: str, + q: str = "", + db: AsyncSession = Depends(get_db), +): + """搜索预注册客户端。""" + rows = ( + await db.execute( + select(PreRegisteredClient).where( + PreRegisteredClient.account_id == account_id + ) + ) + ).scalars().all() + if not q: + return [_to_out(r) for r in rows] + return [ + _to_out(r) for r in rows + if q.lower() in (r.class_identity or "").lower() + ] diff --git a/app/api/management/account_pre_reg_detail.py b/app/api/management/account_pre_reg_detail.py index 9d64d5d..c9ef516 100644 --- a/app/api/management/account_pre_reg_detail.py +++ b/app/api/management/account_pre_reg_detail.py @@ -33,7 +33,7 @@ async def get_pre_reg( return _to_out(row) -@router.post("/{pre_reg_id}/delete", status_code=204) +@router.delete("/{pre_reg_id}", status_code=204) async def delete_pre_reg( account_id: str, pre_reg_id: str, @@ -52,3 +52,27 @@ async def delete_pre_reg( raise HTTPException(404, "预注册客户端不存在") await db.delete(row) await db.commit() + + +@router.post("/{pre_reg_id}/rename") +async def rename_pre_reg(account_id: str, pre_reg_id: str): + """重命名预注册客户端。""" + return {"message": "暂未实现"} + + +@router.post("/{pre_reg_id}/enable") +async def enable_pre_reg(account_id: str, pre_reg_id: str): + """启用预注册客户端。""" + return {"message": "暂未实现"} + + +@router.post("/{pre_reg_id}/disable") +async def disable_pre_reg(account_id: str, pre_reg_id: str): + """禁用预注册客户端。""" + return {"message": "暂未实现"} + + +@router.post("/{pre_reg_id}/config") +async def config_pre_reg(account_id: str, pre_reg_id: str): + """修改预注册客户端使用的档案组。""" + return {"message": "暂未实现"} diff --git a/app/api/management/account_resource.py b/app/api/management/account_resource.py index 4192c8a..b954f4d 100644 --- a/app/api/management/account_resource.py +++ b/app/api/management/account_resource.py @@ -1,7 +1,7 @@ """账户资源管理路由。 提供资源的列表、搜索、创建、上传、删除、重命名、覆盖写入和下载。 -复用 command 模块的 CRUD 逻辑。 +复用 command 模块的数据 CRUD 逻辑。 """ from fastapi import APIRouter diff --git a/app/api/management/account_router.py b/app/api/management/account_router.py index bbcba21..df08d92 100644 --- a/app/api/management/account_router.py +++ b/app/api/management/account_router.py @@ -17,10 +17,15 @@ from .account_pre_reg_crud import router as prereg_r from .account_pre_reg_detail import router as prereg_d_r from .account_pre_reg_preset import router as prereg_p_r +from .account_pre_reg_create import router as prereg_c_r from .account_access import router as access_r +from .account_access_search import router as access_s_r +from .account_access_update import router as access_u_r +from .account_access_delete import router as access_d_r from .account_invitation import router as inv_r +from .account_invitation_create import router as inv_c_r +from .account_invitation_revoke import router as inv_v_r from .bulk import router as bulk_r -from app.api.command.router import router as cmd_r router = APIRouter() @@ -33,8 +38,8 @@ router.include_router(detail_r, prefix="/account", tags=["Account"]) router.include_router(info_r, prefix="/account", tags=["Account"]) -# 资源和客户端(需账户上下文) -_acct = "/accounts/{account_id}" +# 资源(需账户上下文) +_acct = "/account/{account_id}" router.include_router(res_r, prefix=f"{_acct}", tags=["Resource"]) router.include_router(cli_r, prefix=f"{_acct}/client", tags=["Client"]) @@ -47,15 +52,20 @@ router.include_router(prereg_r, prefix=_pre, tags=["PreReg"]) router.include_router(prereg_d_r, prefix=_pre, tags=["PreReg"]) router.include_router(prereg_p_r, prefix=_pre, tags=["PreReg"]) +router.include_router(prereg_c_r, prefix=_pre, tags=["PreReg"]) -# 访问控制和邀请 -router.include_router(access_r, prefix=f"{_acct}/access", tags=["Access"]) -router.include_router(inv_r, prefix=f"{_acct}/invitation", tags=["Invite"]) +# 访问控制 +_acc = f"{_acct}/access" +router.include_router(access_r, prefix=_acc, tags=["Access"]) +router.include_router(access_s_r, prefix=_acc, tags=["Access"]) +router.include_router(access_u_r, prefix=_acc, tags=["Access"]) +router.include_router(access_d_r, prefix=_acc, tags=["Access"]) + +# 邀请 +_inv = f"{_acct}/invitation" +router.include_router(inv_r, prefix=_inv, tags=["Invite"]) +router.include_router(inv_c_r, prefix=_inv, tags=["Invite"]) +router.include_router(inv_v_r, prefix=_inv, tags=["Invite"]) # 批量操作 router.include_router(bulk_r, prefix="/account/bulk", tags=["Bulk"]) - -# 指令/数据操作 -router.include_router( - cmd_r, prefix=f"{_acct}/command", tags=["Command"] -) diff --git a/app/api/management/user_password.py b/app/api/management/user_password.py index a0fa1e3..305c1cf 100644 --- a/app/api/management/user_password.py +++ b/app/api/management/user_password.py @@ -33,3 +33,12 @@ async def change_password( user.hashed_password = hash_password(body.new_password) await db.commit() return {"message": "密码已修改"} + + +@router.post("/reset") +async def reset_password( + db: AsyncSession = Depends(get_db), + uid: str = Depends(get_current_user_id), +): + """重置密码(自助流程,暂未实现详细逻辑)。""" + return {"message": "暂未实现"} diff --git a/app/api/management_config/routes.py b/app/api/management_config/routes.py index c802569..acbe462 100644 --- a/app/api/management_config/routes.py +++ b/app/api/management_config/routes.py @@ -22,9 +22,10 @@ async def get_config( class_identity: str = "", db: AsyncSession = Depends(get_db), ): - """根据子域名自动计算并返回 ManagementPreset.json。 + """根据子域名返回 ManagementPreset.json。 - 若传入 class_identity,尝试匹配预注册客户端。 + 若传入 class_identity 且匹配到预注册客户端, + 额外返回 PreRegisteredLabel 字段。 """ tenant_id = get_tenant_id() stmt = select(Account).where(Account.id == tenant_id) @@ -32,17 +33,16 @@ async def get_config( slug = account.slug if account else "unknown" # 尝试匹配预注册客户端 + label = "" if class_identity and account: - pre_reg = ( - await db.execute( - select(PreRegisteredClient).where( - PreRegisteredClient.account_id == account.id, - PreRegisteredClient.class_identity == class_identity, - ) + pre_reg = (await db.execute( + select(PreRegisteredClient).where( + PreRegisteredClient.account_id == account.id, + PreRegisteredClient.class_identity == class_identity, ) - ).scalar_one_or_none() + )).scalar_one_or_none() if pre_reg: - class_identity = pre_reg.class_identity + label = pre_reg.label return { "IsManagementEnabled": True, @@ -50,5 +50,6 @@ async def get_config( "ManagementServer": f"https://{slug}.{BASE_DOMAIN}", "ManagementServerGrpc": f"grpc://{slug}.{BASE_DOMAIN}", "ClassIdentity": class_identity, + "PreRegisteredLabel": label, "ManifestUrlTemplate": "", } diff --git a/app/api/schemas/access.py b/app/api/schemas/access.py new file mode 100644 index 0000000..1b89c3e --- /dev/null +++ b/app/api/schemas/access.py @@ -0,0 +1,21 @@ +"""访问控制(成员权限)数据传输模型。""" + +from pydantic import BaseModel, Field + + +class AccessMemberOut(BaseModel): + """账户成员响应。""" + + id: str + user_id: str + account_id: str + role_in_account: str + joined_at: str + + +class AccessRoleUpdate(BaseModel): + """修改成员角色请求。""" + + role_in_account: str = Field( + ..., max_length=32, description="owner / admin / member / viewer" + ) diff --git a/app/api/schemas/invitation.py b/app/api/schemas/invitation.py new file mode 100644 index 0000000..4187400 --- /dev/null +++ b/app/api/schemas/invitation.py @@ -0,0 +1,29 @@ +"""邀请相关数据传输模型。""" + +from datetime import datetime +from typing import Optional + +from pydantic import BaseModel, Field + + +class InvitationCreate(BaseModel): + """创建邀请请求。""" + + role_in_account: str = Field("member", max_length=32) + max_uses: int = Field(1, ge=1, le=100) + expires_at: Optional[datetime] = None + + +class InvitationOut(BaseModel): + """邀请响应。""" + + id: str + account_id: str + inviter_user_id: str + code: str + role_in_account: str + max_uses: int + used_count: int + expires_at: Optional[str] = None + is_active: bool + created_at: str diff --git a/app/api/schemas/settings.py b/app/api/schemas/settings.py new file mode 100644 index 0000000..3ad6228 --- /dev/null +++ b/app/api/schemas/settings.py @@ -0,0 +1,32 @@ +"""系统设置请求模型。 + +仅允许白名单内的配置项写入。 +""" + +from pydantic import BaseModel, field_validator, Field + +# 允许写入的配置键白名单 +ALLOWED_KEYS = { + "registration_open", + "require_approval", + "max_accounts_per_user", + "default_role", + "motd", +} + + +class SettingsUpdate(BaseModel): + """系统设置修改请求体。""" + + items: dict[str, str] = Field( + ..., description="键值对,键必须在白名单内" + ) + + @field_validator("items") + @classmethod + def _check_keys(cls, v: dict[str, str]) -> dict[str, str]: + """校验所有 key 是否在白名单中。""" + bad = set(v.keys()) - ALLOWED_KEYS + if bad: + raise ValueError(f"不允许的配置项: {', '.join(bad)}") + return v diff --git a/app/core/auth/admin_middleware.py b/app/core/auth/admin_middleware.py index d6ba1bf..c88b667 100644 --- a/app/core/auth/admin_middleware.py +++ b/app/core/auth/admin_middleware.py @@ -53,8 +53,8 @@ async def dispatch( if user_id: request.state.current_user_id = user_id return await call_next(request) - # 回退:旧式 command 域令牌 - if "/command" in path: + # 回退:旧式 command 域令牌(/account/{id}/... 路径) + if path.startswith("/account/"): ok = await _check_legacy_token(token, path) if ok: return await call_next(request) @@ -80,7 +80,7 @@ async def _check_legacy_token(token: str, path: str = "") -> bool: except RuntimeError: pass if not tid and path: - m = re.match(r"/accounts/([^/]+)/", path) + m = re.match(r"/account/([^/]+)/", path) if m: tid = m.group(1) if not tid: diff --git a/app/grpc/Protobuf/AuditEvent/AppCrashed_pb2.pyi b/app/grpc/Protobuf/AuditEvent/AppCrashed_pb2.pyi new file mode 100644 index 0000000..0572f0c --- /dev/null +++ b/app/grpc/Protobuf/AuditEvent/AppCrashed_pb2.pyi @@ -0,0 +1,11 @@ +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from typing import ClassVar as _ClassVar, Optional as _Optional + +DESCRIPTOR: _descriptor.FileDescriptor + +class AppCrashed(_message.Message): + __slots__ = ("Stacktrace",) + STACKTRACE_FIELD_NUMBER: _ClassVar[int] + Stacktrace: str + def __init__(self, Stacktrace: _Optional[str] = ...) -> None: ... diff --git a/app/grpc/Protobuf/AuditEvent/AppSettingsUpdated_pb2.pyi b/app/grpc/Protobuf/AuditEvent/AppSettingsUpdated_pb2.pyi new file mode 100644 index 0000000..feb62dc --- /dev/null +++ b/app/grpc/Protobuf/AuditEvent/AppSettingsUpdated_pb2.pyi @@ -0,0 +1,11 @@ +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from typing import ClassVar as _ClassVar, Optional as _Optional + +DESCRIPTOR: _descriptor.FileDescriptor + +class AppSettingsUpdated(_message.Message): + __slots__ = ("PropertyName",) + PROPERTYNAME_FIELD_NUMBER: _ClassVar[int] + PropertyName: str + def __init__(self, PropertyName: _Optional[str] = ...) -> None: ... diff --git a/app/grpc/Protobuf/AuditEvent/AuthorizeEvent_pb2.pyi b/app/grpc/Protobuf/AuditEvent/AuthorizeEvent_pb2.pyi new file mode 100644 index 0000000..7b18913 --- /dev/null +++ b/app/grpc/Protobuf/AuditEvent/AuthorizeEvent_pb2.pyi @@ -0,0 +1,11 @@ +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from typing import ClassVar as _ClassVar, Optional as _Optional + +DESCRIPTOR: _descriptor.FileDescriptor + +class AuthorizeEvent(_message.Message): + __slots__ = ("Level",) + LEVEL_FIELD_NUMBER: _ClassVar[int] + Level: int + def __init__(self, Level: _Optional[int] = ...) -> None: ... diff --git a/app/grpc/Protobuf/AuditEvent/ClassChangeCompleted_pb2.pyi b/app/grpc/Protobuf/AuditEvent/ClassChangeCompleted_pb2.pyi new file mode 100644 index 0000000..d04c9fa --- /dev/null +++ b/app/grpc/Protobuf/AuditEvent/ClassChangeCompleted_pb2.pyi @@ -0,0 +1,23 @@ +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from typing import ClassVar as _ClassVar, Optional as _Optional + +DESCRIPTOR: _descriptor.FileDescriptor + +class ClassChangeCompleted(_message.Message): + __slots__ = ("ClassPlanId", "ChangeMode", "SourceClassIndex", "SourceClassSubjectId", "TargetClassIndex", "TargetClassSubjectId", "WriteToSourceClassPlan") + CLASSPLANID_FIELD_NUMBER: _ClassVar[int] + CHANGEMODE_FIELD_NUMBER: _ClassVar[int] + SOURCECLASSINDEX_FIELD_NUMBER: _ClassVar[int] + SOURCECLASSSUBJECTID_FIELD_NUMBER: _ClassVar[int] + TARGETCLASSINDEX_FIELD_NUMBER: _ClassVar[int] + TARGETCLASSSUBJECTID_FIELD_NUMBER: _ClassVar[int] + WRITETOSOURCECLASSPLAN_FIELD_NUMBER: _ClassVar[int] + ClassPlanId: str + ChangeMode: int + SourceClassIndex: int + SourceClassSubjectId: str + TargetClassIndex: int + TargetClassSubjectId: str + WriteToSourceClassPlan: bool + def __init__(self, ClassPlanId: _Optional[str] = ..., ChangeMode: _Optional[int] = ..., SourceClassIndex: _Optional[int] = ..., SourceClassSubjectId: _Optional[str] = ..., TargetClassIndex: _Optional[int] = ..., TargetClassSubjectId: _Optional[str] = ..., WriteToSourceClassPlan: bool = ...) -> None: ... diff --git a/app/grpc/Protobuf/AuditEvent/PluginInstalled_pb2.pyi b/app/grpc/Protobuf/AuditEvent/PluginInstalled_pb2.pyi new file mode 100644 index 0000000..b6b2b5b --- /dev/null +++ b/app/grpc/Protobuf/AuditEvent/PluginInstalled_pb2.pyi @@ -0,0 +1,13 @@ +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from typing import ClassVar as _ClassVar, Optional as _Optional + +DESCRIPTOR: _descriptor.FileDescriptor + +class PluginInstalled(_message.Message): + __slots__ = ("PluginId", "Version") + PLUGINID_FIELD_NUMBER: _ClassVar[int] + VERSION_FIELD_NUMBER: _ClassVar[int] + PluginId: str + Version: str + def __init__(self, PluginId: _Optional[str] = ..., Version: _Optional[str] = ...) -> None: ... diff --git a/app/grpc/Protobuf/AuditEvent/PluginUninstalled_pb2.pyi b/app/grpc/Protobuf/AuditEvent/PluginUninstalled_pb2.pyi new file mode 100644 index 0000000..26c395a --- /dev/null +++ b/app/grpc/Protobuf/AuditEvent/PluginUninstalled_pb2.pyi @@ -0,0 +1,13 @@ +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from typing import ClassVar as _ClassVar, Optional as _Optional + +DESCRIPTOR: _descriptor.FileDescriptor + +class PluginUninstalled(_message.Message): + __slots__ = ("PluginId", "Version") + PLUGINID_FIELD_NUMBER: _ClassVar[int] + VERSION_FIELD_NUMBER: _ClassVar[int] + PluginId: str + Version: str + def __init__(self, PluginId: _Optional[str] = ..., Version: _Optional[str] = ...) -> None: ... diff --git a/app/grpc/Protobuf/AuditEvent/ProfileItemUpdated_pb2.pyi b/app/grpc/Protobuf/AuditEvent/ProfileItemUpdated_pb2.pyi new file mode 100644 index 0000000..b3233df --- /dev/null +++ b/app/grpc/Protobuf/AuditEvent/ProfileItemUpdated_pb2.pyi @@ -0,0 +1,14 @@ +from Protobuf.Enum import ListItemUpdateOperations_pb2 as _ListItemUpdateOperations_pb2 +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union + +DESCRIPTOR: _descriptor.FileDescriptor + +class ProfileItemUpdated(_message.Message): + __slots__ = ("ItemId", "Operation") + ITEMID_FIELD_NUMBER: _ClassVar[int] + OPERATION_FIELD_NUMBER: _ClassVar[int] + ItemId: str + Operation: _ListItemUpdateOperations_pb2.ListItemUpdateOperations + def __init__(self, ItemId: _Optional[str] = ..., Operation: _Optional[_Union[_ListItemUpdateOperations_pb2.ListItemUpdateOperations, str]] = ...) -> None: ... diff --git a/app/grpc/Protobuf/Client/AuditScReq_pb2.pyi b/app/grpc/Protobuf/Client/AuditScReq_pb2.pyi new file mode 100644 index 0000000..88926d9 --- /dev/null +++ b/app/grpc/Protobuf/Client/AuditScReq_pb2.pyi @@ -0,0 +1,16 @@ +from Protobuf.Enum import AuditEvents_pb2 as _AuditEvents_pb2 +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union + +DESCRIPTOR: _descriptor.FileDescriptor + +class AuditScReq(_message.Message): + __slots__ = ("Event", "Payload", "TimestampUtc") + EVENT_FIELD_NUMBER: _ClassVar[int] + PAYLOAD_FIELD_NUMBER: _ClassVar[int] + TIMESTAMPUTC_FIELD_NUMBER: _ClassVar[int] + Event: _AuditEvents_pb2.AuditEvents + Payload: bytes + TimestampUtc: int + def __init__(self, Event: _Optional[_Union[_AuditEvents_pb2.AuditEvents, str]] = ..., Payload: _Optional[bytes] = ..., TimestampUtc: _Optional[int] = ...) -> None: ... diff --git a/app/grpc/Protobuf/Client/ClientCommandDeliverScReq_pb2.pyi b/app/grpc/Protobuf/Client/ClientCommandDeliverScReq_pb2.pyi new file mode 100644 index 0000000..efda4a8 --- /dev/null +++ b/app/grpc/Protobuf/Client/ClientCommandDeliverScReq_pb2.pyi @@ -0,0 +1,14 @@ +from Protobuf.Enum import CommandTypes_pb2 as _CommandTypes_pb2 +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union + +DESCRIPTOR: _descriptor.FileDescriptor + +class ClientCommandDeliverScReq(_message.Message): + __slots__ = ("Type", "Payload") + TYPE_FIELD_NUMBER: _ClassVar[int] + PAYLOAD_FIELD_NUMBER: _ClassVar[int] + Type: _CommandTypes_pb2.CommandTypes + Payload: bytes + def __init__(self, Type: _Optional[_Union[_CommandTypes_pb2.CommandTypes, str]] = ..., Payload: _Optional[bytes] = ...) -> None: ... diff --git a/app/grpc/Protobuf/Client/ClientRegisterCsReq_pb2.pyi b/app/grpc/Protobuf/Client/ClientRegisterCsReq_pb2.pyi new file mode 100644 index 0000000..db4820e --- /dev/null +++ b/app/grpc/Protobuf/Client/ClientRegisterCsReq_pb2.pyi @@ -0,0 +1,15 @@ +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from typing import ClassVar as _ClassVar, Optional as _Optional + +DESCRIPTOR: _descriptor.FileDescriptor + +class ClientRegisterCsReq(_message.Message): + __slots__ = ("ClientUid", "ClientId", "ClientMac") + CLIENTUID_FIELD_NUMBER: _ClassVar[int] + CLIENTID_FIELD_NUMBER: _ClassVar[int] + CLIENTMAC_FIELD_NUMBER: _ClassVar[int] + ClientUid: str + ClientId: str + ClientMac: str + def __init__(self, ClientUid: _Optional[str] = ..., ClientId: _Optional[str] = ..., ClientMac: _Optional[str] = ...) -> None: ... diff --git a/app/grpc/Protobuf/Client/ConfigUploadScReq_pb2.pyi b/app/grpc/Protobuf/Client/ConfigUploadScReq_pb2.pyi new file mode 100644 index 0000000..2640423 --- /dev/null +++ b/app/grpc/Protobuf/Client/ConfigUploadScReq_pb2.pyi @@ -0,0 +1,13 @@ +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from typing import ClassVar as _ClassVar, Optional as _Optional + +DESCRIPTOR: _descriptor.FileDescriptor + +class ConfigUploadScReq(_message.Message): + __slots__ = ("RequestGuidId", "Payload") + REQUESTGUIDID_FIELD_NUMBER: _ClassVar[int] + PAYLOAD_FIELD_NUMBER: _ClassVar[int] + RequestGuidId: str + Payload: str + def __init__(self, RequestGuidId: _Optional[str] = ..., Payload: _Optional[str] = ...) -> None: ... diff --git a/app/grpc/Protobuf/Client/HandshakeScReq_pb2.pyi b/app/grpc/Protobuf/Client/HandshakeScReq_pb2.pyi new file mode 100644 index 0000000..a4664d9 --- /dev/null +++ b/app/grpc/Protobuf/Client/HandshakeScReq_pb2.pyi @@ -0,0 +1,23 @@ +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from typing import ClassVar as _ClassVar, Optional as _Optional + +DESCRIPTOR: _descriptor.FileDescriptor + +class HandshakeScBeginHandShakeReq(_message.Message): + __slots__ = ("ClientUid", "ClientMac", "ChallengeTokenEncrypted", "RequestedServerKeyId") + CLIENTUID_FIELD_NUMBER: _ClassVar[int] + CLIENTMAC_FIELD_NUMBER: _ClassVar[int] + CHALLENGETOKENENCRYPTED_FIELD_NUMBER: _ClassVar[int] + REQUESTEDSERVERKEYID_FIELD_NUMBER: _ClassVar[int] + ClientUid: str + ClientMac: str + ChallengeTokenEncrypted: str + RequestedServerKeyId: int + def __init__(self, ClientUid: _Optional[str] = ..., ClientMac: _Optional[str] = ..., ChallengeTokenEncrypted: _Optional[str] = ..., RequestedServerKeyId: _Optional[int] = ...) -> None: ... + +class HandshakeScCompleteHandshakeReq(_message.Message): + __slots__ = ("Accepted",) + ACCEPTED_FIELD_NUMBER: _ClassVar[int] + Accepted: bool + def __init__(self, Accepted: bool = ...) -> None: ... diff --git a/app/grpc/Protobuf/Command/GetClientConfig_pb2.pyi b/app/grpc/Protobuf/Command/GetClientConfig_pb2.pyi new file mode 100644 index 0000000..2b170ac --- /dev/null +++ b/app/grpc/Protobuf/Command/GetClientConfig_pb2.pyi @@ -0,0 +1,14 @@ +from Protobuf.Enum import ConfigTypes_pb2 as _ConfigTypes_pb2 +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union + +DESCRIPTOR: _descriptor.FileDescriptor + +class GetClientConfig(_message.Message): + __slots__ = ("RequestGuid", "ConfigType") + REQUESTGUID_FIELD_NUMBER: _ClassVar[int] + CONFIGTYPE_FIELD_NUMBER: _ClassVar[int] + RequestGuid: str + ConfigType: _ConfigTypes_pb2.ConfigTypes + def __init__(self, RequestGuid: _Optional[str] = ..., ConfigType: _Optional[_Union[_ConfigTypes_pb2.ConfigTypes, str]] = ...) -> None: ... diff --git a/app/grpc/Protobuf/Command/HeartBeat_pb2.pyi b/app/grpc/Protobuf/Command/HeartBeat_pb2.pyi new file mode 100644 index 0000000..633c09e --- /dev/null +++ b/app/grpc/Protobuf/Command/HeartBeat_pb2.pyi @@ -0,0 +1,11 @@ +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from typing import ClassVar as _ClassVar, Optional as _Optional + +DESCRIPTOR: _descriptor.FileDescriptor + +class HeartBeat(_message.Message): + __slots__ = ("isOnline",) + ISONLINE_FIELD_NUMBER: _ClassVar[int] + isOnline: bool + def __init__(self, isOnline: bool = ...) -> None: ... diff --git a/app/grpc/Protobuf/Command/SendNotification_pb2.pyi b/app/grpc/Protobuf/Command/SendNotification_pb2.pyi new file mode 100644 index 0000000..a53f9c3 --- /dev/null +++ b/app/grpc/Protobuf/Command/SendNotification_pb2.pyi @@ -0,0 +1,31 @@ +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from typing import ClassVar as _ClassVar, Optional as _Optional + +DESCRIPTOR: _descriptor.FileDescriptor + +class SendNotification(_message.Message): + __slots__ = ("MessageMask", "MessageContent", "OverlayIconLeft", "OverlayIconRight", "IsEmergency", "IsSpeechEnabled", "IsEffectEnabled", "IsSoundEnabled", "IsTopmost", "DurationSeconds", "RepeatCounts") + MESSAGEMASK_FIELD_NUMBER: _ClassVar[int] + MESSAGECONTENT_FIELD_NUMBER: _ClassVar[int] + OVERLAYICONLEFT_FIELD_NUMBER: _ClassVar[int] + OVERLAYICONRIGHT_FIELD_NUMBER: _ClassVar[int] + ISEMERGENCY_FIELD_NUMBER: _ClassVar[int] + ISSPEECHENABLED_FIELD_NUMBER: _ClassVar[int] + ISEFFECTENABLED_FIELD_NUMBER: _ClassVar[int] + ISSOUNDENABLED_FIELD_NUMBER: _ClassVar[int] + ISTOPMOST_FIELD_NUMBER: _ClassVar[int] + DURATIONSECONDS_FIELD_NUMBER: _ClassVar[int] + REPEATCOUNTS_FIELD_NUMBER: _ClassVar[int] + MessageMask: str + MessageContent: str + OverlayIconLeft: int + OverlayIconRight: int + IsEmergency: bool + IsSpeechEnabled: bool + IsEffectEnabled: bool + IsSoundEnabled: bool + IsTopmost: bool + DurationSeconds: float + RepeatCounts: int + def __init__(self, MessageMask: _Optional[str] = ..., MessageContent: _Optional[str] = ..., OverlayIconLeft: _Optional[int] = ..., OverlayIconRight: _Optional[int] = ..., IsEmergency: bool = ..., IsSpeechEnabled: bool = ..., IsEffectEnabled: bool = ..., IsSoundEnabled: bool = ..., IsTopmost: bool = ..., DurationSeconds: _Optional[float] = ..., RepeatCounts: _Optional[int] = ...) -> None: ... diff --git a/app/grpc/Protobuf/Enum/AuditEvents_pb2.pyi b/app/grpc/Protobuf/Enum/AuditEvents_pb2.pyi new file mode 100644 index 0000000..2a9b6d7 --- /dev/null +++ b/app/grpc/Protobuf/Enum/AuditEvents_pb2.pyi @@ -0,0 +1,34 @@ +from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper +from google.protobuf import descriptor as _descriptor +from typing import ClassVar as _ClassVar + +DESCRIPTOR: _descriptor.FileDescriptor + +class AuditEvents(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + DefaultEvent: _ClassVar[AuditEvents] + AuthorizeSuccess: _ClassVar[AuditEvents] + AuthorizeFailed: _ClassVar[AuditEvents] + AppSettingsUpdated: _ClassVar[AuditEvents] + ClassChangeCompleted: _ClassVar[AuditEvents] + ClassPlanUpdated: _ClassVar[AuditEvents] + TimeLayoutUpdated: _ClassVar[AuditEvents] + SubjectUpdated: _ClassVar[AuditEvents] + AppCrashed: _ClassVar[AuditEvents] + AppStarted: _ClassVar[AuditEvents] + AppExited: _ClassVar[AuditEvents] + PluginInstalled: _ClassVar[AuditEvents] + PluginUninstalled: _ClassVar[AuditEvents] +DefaultEvent: AuditEvents +AuthorizeSuccess: AuditEvents +AuthorizeFailed: AuditEvents +AppSettingsUpdated: AuditEvents +ClassChangeCompleted: AuditEvents +ClassPlanUpdated: AuditEvents +TimeLayoutUpdated: AuditEvents +SubjectUpdated: AuditEvents +AppCrashed: AuditEvents +AppStarted: AuditEvents +AppExited: AuditEvents +PluginInstalled: AuditEvents +PluginUninstalled: AuditEvents diff --git a/app/grpc/Protobuf/Enum/CommandTypes_pb2.pyi b/app/grpc/Protobuf/Enum/CommandTypes_pb2.pyi new file mode 100644 index 0000000..8957a6a --- /dev/null +++ b/app/grpc/Protobuf/Enum/CommandTypes_pb2.pyi @@ -0,0 +1,22 @@ +from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper +from google.protobuf import descriptor as _descriptor +from typing import ClassVar as _ClassVar + +DESCRIPTOR: _descriptor.FileDescriptor + +class CommandTypes(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + DefaultCommand: _ClassVar[CommandTypes] + Ping: _ClassVar[CommandTypes] + Pong: _ClassVar[CommandTypes] + RestartApp: _ClassVar[CommandTypes] + SendNotification: _ClassVar[CommandTypes] + DataUpdated: _ClassVar[CommandTypes] + GetClientConfig: _ClassVar[CommandTypes] +DefaultCommand: CommandTypes +Ping: CommandTypes +Pong: CommandTypes +RestartApp: CommandTypes +SendNotification: CommandTypes +DataUpdated: CommandTypes +GetClientConfig: CommandTypes diff --git a/app/grpc/Protobuf/Enum/ConfigTypes_pb2.pyi b/app/grpc/Protobuf/Enum/ConfigTypes_pb2.pyi new file mode 100644 index 0000000..a0ee8be --- /dev/null +++ b/app/grpc/Protobuf/Enum/ConfigTypes_pb2.pyi @@ -0,0 +1,22 @@ +from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper +from google.protobuf import descriptor as _descriptor +from typing import ClassVar as _ClassVar + +DESCRIPTOR: _descriptor.FileDescriptor + +class ConfigTypes(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + UnspecifiedConfig: _ClassVar[ConfigTypes] + AppSettings: _ClassVar[ConfigTypes] + Profile: _ClassVar[ConfigTypes] + CurrentComponent: _ClassVar[ConfigTypes] + CurrentAutomation: _ClassVar[ConfigTypes] + Logs: _ClassVar[ConfigTypes] + PluginList: _ClassVar[ConfigTypes] +UnspecifiedConfig: ConfigTypes +AppSettings: ConfigTypes +Profile: ConfigTypes +CurrentComponent: ConfigTypes +CurrentAutomation: ConfigTypes +Logs: ConfigTypes +PluginList: ConfigTypes diff --git a/app/grpc/Protobuf/Enum/ListItemUpdateOperations_pb2.pyi b/app/grpc/Protobuf/Enum/ListItemUpdateOperations_pb2.pyi new file mode 100644 index 0000000..a12f318 --- /dev/null +++ b/app/grpc/Protobuf/Enum/ListItemUpdateOperations_pb2.pyi @@ -0,0 +1,16 @@ +from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper +from google.protobuf import descriptor as _descriptor +from typing import ClassVar as _ClassVar + +DESCRIPTOR: _descriptor.FileDescriptor + +class ListItemUpdateOperations(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + ListItemUpdateOperationsUnspecified: _ClassVar[ListItemUpdateOperations] + Add: _ClassVar[ListItemUpdateOperations] + Update: _ClassVar[ListItemUpdateOperations] + Remove: _ClassVar[ListItemUpdateOperations] +ListItemUpdateOperationsUnspecified: ListItemUpdateOperations +Add: ListItemUpdateOperations +Update: ListItemUpdateOperations +Remove: ListItemUpdateOperations diff --git a/app/grpc/Protobuf/Enum/Retcode_pb2.pyi b/app/grpc/Protobuf/Enum/Retcode_pb2.pyi new file mode 100644 index 0000000..b6d8643 --- /dev/null +++ b/app/grpc/Protobuf/Enum/Retcode_pb2.pyi @@ -0,0 +1,24 @@ +from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper +from google.protobuf import descriptor as _descriptor +from typing import ClassVar as _ClassVar + +DESCRIPTOR: _descriptor.FileDescriptor + +class Retcode(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + Unspecified: _ClassVar[Retcode] + Success: _ClassVar[Retcode] + ServerInternalError: _ClassVar[Retcode] + InvalidRequest: _ClassVar[Retcode] + HandshakeClientRejected: _ClassVar[Retcode] + Registered: _ClassVar[Retcode] + ClientNotFound: _ClassVar[Retcode] + PairingRequired: _ClassVar[Retcode] +Unspecified: Retcode +Success: Retcode +ServerInternalError: Retcode +InvalidRequest: Retcode +HandshakeClientRejected: Retcode +Registered: Retcode +ClientNotFound: Retcode +PairingRequired: Retcode diff --git a/app/grpc/Protobuf/Server/AuditScRsp_pb2.pyi b/app/grpc/Protobuf/Server/AuditScRsp_pb2.pyi new file mode 100644 index 0000000..0a13c28 --- /dev/null +++ b/app/grpc/Protobuf/Server/AuditScRsp_pb2.pyi @@ -0,0 +1,14 @@ +from Protobuf.Enum import Retcode_pb2 as _Retcode_pb2 +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union + +DESCRIPTOR: _descriptor.FileDescriptor + +class AuditScRsp(_message.Message): + __slots__ = ("Retcode", "Message") + RETCODE_FIELD_NUMBER: _ClassVar[int] + MESSAGE_FIELD_NUMBER: _ClassVar[int] + Retcode: _Retcode_pb2.Retcode + Message: str + def __init__(self, Retcode: _Optional[_Union[_Retcode_pb2.Retcode, str]] = ..., Message: _Optional[str] = ...) -> None: ... diff --git a/app/grpc/Protobuf/Server/ClientCommandDeliverScRsp_pb2.pyi b/app/grpc/Protobuf/Server/ClientCommandDeliverScRsp_pb2.pyi new file mode 100644 index 0000000..c3737f7 --- /dev/null +++ b/app/grpc/Protobuf/Server/ClientCommandDeliverScRsp_pb2.pyi @@ -0,0 +1,17 @@ +from Protobuf.Enum import CommandTypes_pb2 as _CommandTypes_pb2 +from Protobuf.Enum import Retcode_pb2 as _Retcode_pb2 +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union + +DESCRIPTOR: _descriptor.FileDescriptor + +class ClientCommandDeliverScRsp(_message.Message): + __slots__ = ("RetCode", "Type", "Payload") + RETCODE_FIELD_NUMBER: _ClassVar[int] + TYPE_FIELD_NUMBER: _ClassVar[int] + PAYLOAD_FIELD_NUMBER: _ClassVar[int] + RetCode: _Retcode_pb2.Retcode + Type: _CommandTypes_pb2.CommandTypes + Payload: bytes + def __init__(self, RetCode: _Optional[_Union[_Retcode_pb2.Retcode, str]] = ..., Type: _Optional[_Union[_CommandTypes_pb2.CommandTypes, str]] = ..., Payload: _Optional[bytes] = ...) -> None: ... diff --git a/app/grpc/Protobuf/Server/ClientRegisterScRsp_pb2.pyi b/app/grpc/Protobuf/Server/ClientRegisterScRsp_pb2.pyi new file mode 100644 index 0000000..4159b37 --- /dev/null +++ b/app/grpc/Protobuf/Server/ClientRegisterScRsp_pb2.pyi @@ -0,0 +1,16 @@ +from Protobuf.Enum import Retcode_pb2 as _Retcode_pb2 +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union + +DESCRIPTOR: _descriptor.FileDescriptor + +class ClientRegisterScRsp(_message.Message): + __slots__ = ("Retcode", "Message", "ServerPublicKey") + RETCODE_FIELD_NUMBER: _ClassVar[int] + MESSAGE_FIELD_NUMBER: _ClassVar[int] + SERVERPUBLICKEY_FIELD_NUMBER: _ClassVar[int] + Retcode: _Retcode_pb2.Retcode + Message: str + ServerPublicKey: str + def __init__(self, Retcode: _Optional[_Union[_Retcode_pb2.Retcode, str]] = ..., Message: _Optional[str] = ..., ServerPublicKey: _Optional[str] = ...) -> None: ... diff --git a/app/grpc/Protobuf/Server/ConfigUploadScRsp_pb2.pyi b/app/grpc/Protobuf/Server/ConfigUploadScRsp_pb2.pyi new file mode 100644 index 0000000..313833a --- /dev/null +++ b/app/grpc/Protobuf/Server/ConfigUploadScRsp_pb2.pyi @@ -0,0 +1,14 @@ +from Protobuf.Enum import Retcode_pb2 as _Retcode_pb2 +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union + +DESCRIPTOR: _descriptor.FileDescriptor + +class ConfigUploadScRsp(_message.Message): + __slots__ = ("Retcode", "Message") + RETCODE_FIELD_NUMBER: _ClassVar[int] + MESSAGE_FIELD_NUMBER: _ClassVar[int] + Retcode: _Retcode_pb2.Retcode + Message: str + def __init__(self, Retcode: _Optional[_Union[_Retcode_pb2.Retcode, str]] = ..., Message: _Optional[str] = ...) -> None: ... diff --git a/app/grpc/Protobuf/Server/HandshakeScRsp_pb2.pyi b/app/grpc/Protobuf/Server/HandshakeScRsp_pb2.pyi new file mode 100644 index 0000000..e0c56ff --- /dev/null +++ b/app/grpc/Protobuf/Server/HandshakeScRsp_pb2.pyi @@ -0,0 +1,28 @@ +from Protobuf.Enum import Retcode_pb2 as _Retcode_pb2 +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union + +DESCRIPTOR: _descriptor.FileDescriptor + +class HandshakeScBeginHandShakeRsp(_message.Message): + __slots__ = ("Retcode", "Message", "ChallengeTokenDecrypted", "ServerPublicKey") + RETCODE_FIELD_NUMBER: _ClassVar[int] + MESSAGE_FIELD_NUMBER: _ClassVar[int] + CHALLENGETOKENDECRYPTED_FIELD_NUMBER: _ClassVar[int] + SERVERPUBLICKEY_FIELD_NUMBER: _ClassVar[int] + Retcode: _Retcode_pb2.Retcode + Message: str + ChallengeTokenDecrypted: str + ServerPublicKey: str + def __init__(self, Retcode: _Optional[_Union[_Retcode_pb2.Retcode, str]] = ..., Message: _Optional[str] = ..., ChallengeTokenDecrypted: _Optional[str] = ..., ServerPublicKey: _Optional[str] = ...) -> None: ... + +class HandshakeScCompleteHandshakeRsp(_message.Message): + __slots__ = ("Retcode", "Message", "SessionId") + RETCODE_FIELD_NUMBER: _ClassVar[int] + MESSAGE_FIELD_NUMBER: _ClassVar[int] + SESSIONID_FIELD_NUMBER: _ClassVar[int] + Retcode: _Retcode_pb2.Retcode + Message: str + SessionId: str + def __init__(self, Retcode: _Optional[_Union[_Retcode_pb2.Retcode, str]] = ..., Message: _Optional[str] = ..., SessionId: _Optional[str] = ...) -> None: ... diff --git a/app/grpc/Protobuf/Service/Audit_pb2.pyi b/app/grpc/Protobuf/Service/Audit_pb2.pyi new file mode 100644 index 0000000..c2ef398 --- /dev/null +++ b/app/grpc/Protobuf/Service/Audit_pb2.pyi @@ -0,0 +1,6 @@ +from Protobuf.Client import AuditScReq_pb2 as _AuditScReq_pb2 +from Protobuf.Server import AuditScRsp_pb2 as _AuditScRsp_pb2 +from google.protobuf import descriptor as _descriptor +from typing import ClassVar as _ClassVar + +DESCRIPTOR: _descriptor.FileDescriptor diff --git a/app/grpc/Protobuf/Service/ClientCommandDeliver_pb2.pyi b/app/grpc/Protobuf/Service/ClientCommandDeliver_pb2.pyi new file mode 100644 index 0000000..094ea76 --- /dev/null +++ b/app/grpc/Protobuf/Service/ClientCommandDeliver_pb2.pyi @@ -0,0 +1,6 @@ +from Protobuf.Server import ClientCommandDeliverScRsp_pb2 as _ClientCommandDeliverScRsp_pb2 +from Protobuf.Client import ClientCommandDeliverScReq_pb2 as _ClientCommandDeliverScReq_pb2 +from google.protobuf import descriptor as _descriptor +from typing import ClassVar as _ClassVar + +DESCRIPTOR: _descriptor.FileDescriptor diff --git a/app/grpc/Protobuf/Service/ClientRegister_pb2.pyi b/app/grpc/Protobuf/Service/ClientRegister_pb2.pyi new file mode 100644 index 0000000..fd769d0 --- /dev/null +++ b/app/grpc/Protobuf/Service/ClientRegister_pb2.pyi @@ -0,0 +1,6 @@ +from Protobuf.Client import ClientRegisterCsReq_pb2 as _ClientRegisterCsReq_pb2 +from Protobuf.Server import ClientRegisterScRsp_pb2 as _ClientRegisterScRsp_pb2 +from google.protobuf import descriptor as _descriptor +from typing import ClassVar as _ClassVar + +DESCRIPTOR: _descriptor.FileDescriptor diff --git a/app/grpc/Protobuf/Service/ConfigUpload_pb2.pyi b/app/grpc/Protobuf/Service/ConfigUpload_pb2.pyi new file mode 100644 index 0000000..178e3c5 --- /dev/null +++ b/app/grpc/Protobuf/Service/ConfigUpload_pb2.pyi @@ -0,0 +1,6 @@ +from Protobuf.Server import ConfigUploadScRsp_pb2 as _ConfigUploadScRsp_pb2 +from Protobuf.Client import ConfigUploadScReq_pb2 as _ConfigUploadScReq_pb2 +from google.protobuf import descriptor as _descriptor +from typing import ClassVar as _ClassVar + +DESCRIPTOR: _descriptor.FileDescriptor diff --git a/app/grpc/Protobuf/Service/Handshake_pb2.pyi b/app/grpc/Protobuf/Service/Handshake_pb2.pyi new file mode 100644 index 0000000..5efa7ba --- /dev/null +++ b/app/grpc/Protobuf/Service/Handshake_pb2.pyi @@ -0,0 +1,6 @@ +from Protobuf.Client import HandshakeScReq_pb2 as _HandshakeScReq_pb2 +from Protobuf.Server import HandshakeScRsp_pb2 as _HandshakeScRsp_pb2 +from google.protobuf import descriptor as _descriptor +from typing import ClassVar as _ClassVar + +DESCRIPTOR: _descriptor.FileDescriptor diff --git a/app/models/__init__.py b/app/models/__init__.py index d3224d9..95d4111 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -28,6 +28,7 @@ from .pairing import PairingCode from .role_permission import RolePermission from .pre_registered_client import PreRegisteredClient +from .invitation import Invitation from .engine import AsyncSessionLocal, init_db from .session import get_db @@ -56,6 +57,7 @@ "PairingCode", "RolePermission", "PreRegisteredClient", + "Invitation", "AsyncSessionLocal", "init_db", "get_db", diff --git a/app/models/invitation.py b/app/models/invitation.py new file mode 100644 index 0000000..43c735b --- /dev/null +++ b/app/models/invitation.py @@ -0,0 +1,38 @@ +"""邀请码模型。 + +存储账户级邀请码,支持多次使用和过期控制。 +""" + +import secrets +import uuid +from datetime import datetime +from typing import Optional + +from sqlalchemy import Boolean, DateTime, Integer, String +from sqlalchemy.orm import Mapped, mapped_column + +from .base import Base + + +class Invitation(Base): + """账户邀请码记录。""" + + __tablename__ = "invitations" + + id: Mapped[str] = mapped_column( + String, primary_key=True, default=lambda: str(uuid.uuid4()) + ) + account_id: Mapped[str] = mapped_column(String, index=True) + inviter_user_id: Mapped[str] = mapped_column(String) + code: Mapped[str] = mapped_column( + String(32), unique=True, index=True, + default=lambda: secrets.token_urlsafe(12)[:16], + ) + role_in_account: Mapped[str] = mapped_column(String(32), default="member") + max_uses: Mapped[int] = mapped_column(Integer, default=1) + used_count: Mapped[int] = mapped_column(Integer, default=0) + expires_at: Mapped[Optional[datetime]] = mapped_column( + DateTime(timezone=True), nullable=True + ) + is_active: Mapped[bool] = mapped_column(Boolean, default=True) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True)) diff --git a/app/models/schema_init.py b/app/models/schema_init.py index e9d59e0..74201b7 100644 --- a/app/models/schema_init.py +++ b/app/models/schema_init.py @@ -21,6 +21,7 @@ "account_quotas", "system_config", "reserved_names", + "invitations", } diff --git a/tests/test_api.py b/tests/test_api.py index e9e123d..9cc9b29 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -35,7 +35,7 @@ async def lifespan(): # ---- 路径前缀常量 ---- -_CMD_PREFIX = f"/accounts/{TEST_ACCOUNT_ID}/command" +_RES_PREFIX = f"/account/{TEST_ACCOUNT_ID}" @pytest.mark.asyncio @@ -69,7 +69,7 @@ async def test_command_endpoints(command_headers): ) as ac: # Create a ClassPlan create_res = await ac.get( - f"{_CMD_PREFIX}/datas/ClassPlan/create?name=test_cp", + f"{_RES_PREFIX}/ClassPlan/create?name=test_cp", headers=command_headers, ) assert create_res.status_code == 200 @@ -77,14 +77,14 @@ async def test_command_endpoints(command_headers): # List it list_res = await ac.get( - f"{_CMD_PREFIX}/datas/ClassPlan/list", headers=command_headers + f"{_RES_PREFIX}/ClassPlan/list", headers=command_headers ) assert list_res.status_code == 200 assert "test_cp" in list_res.json() # Delete it del_res = await ac.get( - f"{_CMD_PREFIX}/datas/ClassPlan/delete?name=test_cp", + f"{_RES_PREFIX}/ClassPlan/delete?name=test_cp", headers=command_headers, ) assert del_res.status_code == 200 @@ -92,13 +92,13 @@ async def test_command_endpoints(command_headers): # Create again for write tests await ac.get( - f"{_CMD_PREFIX}/datas/ClassPlan/create?name=test_cp2", + f"{_RES_PREFIX}/ClassPlan/create?name=test_cp2", headers=command_headers, ) # Write write_res = await ac.put( - f"{_CMD_PREFIX}/datas/ClassPlan/write?name=test_cp2", + f"{_RES_PREFIX}/ClassPlan/write?name=test_cp2", json={"nested": {"value": 1}, "preserve": True}, headers=command_headers, ) @@ -106,7 +106,7 @@ async def test_command_endpoints(command_headers): # Deep Merge Update patch_res = await ac.patch( - f"{_CMD_PREFIX}/datas/ClassPlan/update?name=test_cp2", + f"{_RES_PREFIX}/ClassPlan/update?name=test_cp2", json={"nested": {"value": 2}, "new_key": "exists"}, headers=command_headers, ) @@ -114,7 +114,7 @@ async def test_command_endpoints(command_headers): # Validate via token endpoint token_res = await ac.get( - f"{_CMD_PREFIX}/datas/ClassPlan/token?name=test_cp2", + f"{_RES_PREFIX}/ClassPlan/token?name=test_cp2", headers=command_headers, ) assert token_res.status_code == 200 @@ -149,7 +149,7 @@ async def test_command_endpoints(command_headers): ] } batch_res = await ac.post( - f"{_CMD_PREFIX}/datas/batch", + f"{_RES_PREFIX}/batch", json=batch_payload, headers=command_headers, ) @@ -157,12 +157,12 @@ async def test_command_endpoints(command_headers): assert batch_res.json()["status"] == "success" list_tl = await ac.get( - f"{_CMD_PREFIX}/datas/TimeLayout/list", headers=command_headers + f"{_RES_PREFIX}/TimeLayout/list", headers=command_headers ) assert "test_batch_layout" in list_tl.json() list_cp = await ac.get( - f"{_CMD_PREFIX}/datas/ClassPlan/list", headers=command_headers + f"{_RES_PREFIX}/ClassPlan/list", headers=command_headers ) assert "test_cp2" not in list_cp.json() @@ -230,18 +230,18 @@ async def test_get_client_resource(): ) as ac: # Create and write await ac.get( - f"{_CMD_PREFIX}/datas/ClassPlan/create?name=valid_cp_{test_uid}", + f"{_RES_PREFIX}/ClassPlan/create?name=valid_cp_{test_uid}", headers=cmd_headers, ) await ac.put( - f"{_CMD_PREFIX}/datas/ClassPlan/write?name=valid_cp_{test_uid}", + f"{_RES_PREFIX}/ClassPlan/write?name=valid_cp_{test_uid}", json={"valid": True}, headers=cmd_headers, ) # Get token token_res = await ac.get( - f"{_CMD_PREFIX}/datas/ClassPlan/token?name=valid_cp_{test_uid}", + f"{_RES_PREFIX}/ClassPlan/token?name=valid_cp_{test_uid}", headers=cmd_headers, ) assert token_res.status_code == 200 @@ -301,7 +301,7 @@ async def test_get_endpoint_corrupted_resource(): transport=mgr_transport, base_url="http://test" ) as ac: token_res = await ac.get( - f"{_CMD_PREFIX}/datas/ClassPlan/token?name=corrupted_cp_{test_uid}", + f"{_RES_PREFIX}/ClassPlan/token?name=corrupted_cp_{test_uid}", headers=cmd_headers, ) assert token_res.status_code == 200 @@ -332,31 +332,31 @@ async def test_command_token_endpoint(): ) as ac: # Invalid resource type res = await ac.get( - f"{_CMD_PREFIX}/datas/InvalidRes/token?name=test", + f"{_RES_PREFIX}/InvalidRes/token?name=test", headers=cmd_headers, ) assert res.json()["status"] == "error" # Non-existent resource res = await ac.get( - f"{_CMD_PREFIX}/datas/ClassPlan/token?name=nonexist_{test_uid}", + f"{_RES_PREFIX}/ClassPlan/token?name=nonexist_{test_uid}", headers=cmd_headers, ) assert res.json()["status"] == "error" # Valid resource — get token and use it await ac.get( - f"{_CMD_PREFIX}/datas/ClassPlan/create?name=token_cp_{test_uid}", + f"{_RES_PREFIX}/ClassPlan/create?name=token_cp_{test_uid}", headers=cmd_headers, ) await ac.put( - f"{_CMD_PREFIX}/datas/ClassPlan/write?name=token_cp_{test_uid}", + f"{_RES_PREFIX}/ClassPlan/write?name=token_cp_{test_uid}", json={"hello": "world"}, headers=cmd_headers, ) token_res = await ac.get( - f"{_CMD_PREFIX}/datas/ClassPlan/token?name=token_cp_{test_uid}", + f"{_RES_PREFIX}/ClassPlan/token?name=token_cp_{test_uid}", headers=cmd_headers, ) assert token_res.status_code == 200 @@ -396,16 +396,16 @@ async def test_get_ip_auth_pass(): transport=mgr_transport, base_url="http://test" ) as ac: await ac.get( - f"{_CMD_PREFIX}/datas/ClassPlan/create?name=ip_pass_{test_uid}", + f"{_RES_PREFIX}/ClassPlan/create?name=ip_pass_{test_uid}", headers=cmd_headers, ) await ac.put( - f"{_CMD_PREFIX}/datas/ClassPlan/write?name=ip_pass_{test_uid}", + f"{_RES_PREFIX}/ClassPlan/write?name=ip_pass_{test_uid}", json={"ip_test": "pass"}, headers=cmd_headers, ) token_res = await ac.get( - f"{_CMD_PREFIX}/datas/ClassPlan/token?name=ip_pass_{test_uid}", + f"{_RES_PREFIX}/ClassPlan/token?name=ip_pass_{test_uid}", headers=cmd_headers, ) token_url = token_res.json()["url"] @@ -437,11 +437,11 @@ async def test_get_ip_auth_fail(): transport=mgr_transport, base_url="http://test" ) as ac: await ac.get( - f"{_CMD_PREFIX}/datas/ClassPlan/create?name=ip_fail_{test_uid}", + f"{_RES_PREFIX}/ClassPlan/create?name=ip_fail_{test_uid}", headers=cmd_headers, ) await ac.put( - f"{_CMD_PREFIX}/datas/ClassPlan/write?name=ip_fail_{test_uid}", + f"{_RES_PREFIX}/ClassPlan/write?name=ip_fail_{test_uid}", json={"ip_test": "should_not_see"}, headers=cmd_headers, ) @@ -493,7 +493,7 @@ async def test_command_without_token(): async with AsyncClient( transport=transport, base_url="http://test" ) as ac: - res = await ac.get(f"{_CMD_PREFIX}/datas/ClassPlan/list") + res = await ac.get(f"{_RES_PREFIX}/ClassPlan/list") assert res.status_code == 401 @@ -505,7 +505,7 @@ async def test_command_with_invalid_token(): transport=transport, base_url="http://test" ) as ac: res = await ac.get( - f"{_CMD_PREFIX}/datas/ClassPlan/list", + f"{_RES_PREFIX}/ClassPlan/list", headers={"Authorization": "Bearer invalid_token_here"}, ) assert res.status_code == 403 diff --git a/uv.lock b/uv.lock index 7b8c8a2..3aae776 100644 --- a/uv.lock +++ b/uv.lock @@ -461,62 +461,62 @@ toml = [ [[package]] name = "cryptography" -version = "46.0.5" +version = "46.0.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/60/04/ee2a9e8542e4fa2773b81771ff8349ff19cdd56b7258a0cc442639052edb/cryptography-46.0.5.tar.gz", hash = "sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d", size = 750064, upload-time = "2026-02-10T19:18:38.255Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/81/b0bb27f2ba931a65409c6b8a8b358a7f03c0e46eceacddff55f7c84b1f3b/cryptography-46.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:351695ada9ea9618b3500b490ad54c739860883df6c1f555e088eaf25b1bbaad", size = 7176289, upload-time = "2026-02-10T19:17:08.274Z" }, - { url = "https://files.pythonhosted.org/packages/ff/9e/6b4397a3e3d15123de3b1806ef342522393d50736c13b20ec4c9ea6693a6/cryptography-46.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c18ff11e86df2e28854939acde2d003f7984f721eba450b56a200ad90eeb0e6b", size = 4275637, upload-time = "2026-02-10T19:17:10.53Z" }, - { url = "https://files.pythonhosted.org/packages/63/e7/471ab61099a3920b0c77852ea3f0ea611c9702f651600397ac567848b897/cryptography-46.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d7e3d356b8cd4ea5aff04f129d5f66ebdc7b6f8eae802b93739ed520c47c79b", size = 4424742, upload-time = "2026-02-10T19:17:12.388Z" }, - { url = "https://files.pythonhosted.org/packages/37/53/a18500f270342d66bf7e4d9f091114e31e5ee9e7375a5aba2e85a91e0044/cryptography-46.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263", size = 4277528, upload-time = "2026-02-10T19:17:13.853Z" }, - { url = "https://files.pythonhosted.org/packages/22/29/c2e812ebc38c57b40e7c583895e73c8c5adb4d1e4a0cc4c5a4fdab2b1acc/cryptography-46.0.5-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:803812e111e75d1aa73690d2facc295eaefd4439be1023fefc4995eaea2af90d", size = 4947993, upload-time = "2026-02-10T19:17:15.618Z" }, - { url = "https://files.pythonhosted.org/packages/6b/e7/237155ae19a9023de7e30ec64e5d99a9431a567407ac21170a046d22a5a3/cryptography-46.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed", size = 4456855, upload-time = "2026-02-10T19:17:17.221Z" }, - { url = "https://files.pythonhosted.org/packages/2d/87/fc628a7ad85b81206738abbd213b07702bcbdada1dd43f72236ef3cffbb5/cryptography-46.0.5-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:f145bba11b878005c496e93e257c1e88f154d278d2638e6450d17e0f31e558d2", size = 3984635, upload-time = "2026-02-10T19:17:18.792Z" }, - { url = "https://files.pythonhosted.org/packages/84/29/65b55622bde135aedf4565dc509d99b560ee4095e56989e815f8fd2aa910/cryptography-46.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e9251e3be159d1020c4030bd2e5f84d6a43fe54b6c19c12f51cde9542a2817b2", size = 4277038, upload-time = "2026-02-10T19:17:20.256Z" }, - { url = "https://files.pythonhosted.org/packages/bc/36/45e76c68d7311432741faf1fbf7fac8a196a0a735ca21f504c75d37e2558/cryptography-46.0.5-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:47fb8a66058b80e509c47118ef8a75d14c455e81ac369050f20ba0d23e77fee0", size = 4912181, upload-time = "2026-02-10T19:17:21.825Z" }, - { url = "https://files.pythonhosted.org/packages/6d/1a/c1ba8fead184d6e3d5afcf03d569acac5ad063f3ac9fb7258af158f7e378/cryptography-46.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4c3341037c136030cb46e4b1e17b7418ea4cbd9dd207e4a6f3b2b24e0d4ac731", size = 4456482, upload-time = "2026-02-10T19:17:25.133Z" }, - { url = "https://files.pythonhosted.org/packages/f9/e5/3fb22e37f66827ced3b902cf895e6a6bc1d095b5b26be26bd13c441fdf19/cryptography-46.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:890bcb4abd5a2d3f852196437129eb3667d62630333aacc13dfd470fad3aaa82", size = 4405497, upload-time = "2026-02-10T19:17:26.66Z" }, - { url = "https://files.pythonhosted.org/packages/1a/df/9d58bb32b1121a8a2f27383fabae4d63080c7ca60b9b5c88be742be04ee7/cryptography-46.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80a8d7bfdf38f87ca30a5391c0c9ce4ed2926918e017c29ddf643d0ed2778ea1", size = 4667819, upload-time = "2026-02-10T19:17:28.569Z" }, - { url = "https://files.pythonhosted.org/packages/ea/ed/325d2a490c5e94038cdb0117da9397ece1f11201f425c4e9c57fe5b9f08b/cryptography-46.0.5-cp311-abi3-win32.whl", hash = "sha256:60ee7e19e95104d4c03871d7d7dfb3d22ef8a9b9c6778c94e1c8fcc8365afd48", size = 3028230, upload-time = "2026-02-10T19:17:30.518Z" }, - { url = "https://files.pythonhosted.org/packages/e9/5a/ac0f49e48063ab4255d9e3b79f5def51697fce1a95ea1370f03dc9db76f6/cryptography-46.0.5-cp311-abi3-win_amd64.whl", hash = "sha256:38946c54b16c885c72c4f59846be9743d699eee2b69b6988e0a00a01f46a61a4", size = 3480909, upload-time = "2026-02-10T19:17:32.083Z" }, - { url = "https://files.pythonhosted.org/packages/00/13/3d278bfa7a15a96b9dc22db5a12ad1e48a9eb3d40e1827ef66a5df75d0d0/cryptography-46.0.5-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:94a76daa32eb78d61339aff7952ea819b1734b46f73646a07decb40e5b3448e2", size = 7119287, upload-time = "2026-02-10T19:17:33.801Z" }, - { url = "https://files.pythonhosted.org/packages/67/c8/581a6702e14f0898a0848105cbefd20c058099e2c2d22ef4e476dfec75d7/cryptography-46.0.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5be7bf2fb40769e05739dd0046e7b26f9d4670badc7b032d6ce4db64dddc0678", size = 4265728, upload-time = "2026-02-10T19:17:35.569Z" }, - { url = "https://files.pythonhosted.org/packages/dd/4a/ba1a65ce8fc65435e5a849558379896c957870dd64fecea97b1ad5f46a37/cryptography-46.0.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe346b143ff9685e40192a4960938545c699054ba11d4f9029f94751e3f71d87", size = 4408287, upload-time = "2026-02-10T19:17:36.938Z" }, - { url = "https://files.pythonhosted.org/packages/f8/67/8ffdbf7b65ed1ac224d1c2df3943553766914a8ca718747ee3871da6107e/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c69fd885df7d089548a42d5ec05be26050ebcd2283d89b3d30676eb32ff87dee", size = 4270291, upload-time = "2026-02-10T19:17:38.748Z" }, - { url = "https://files.pythonhosted.org/packages/f8/e5/f52377ee93bc2f2bba55a41a886fd208c15276ffbd2569f2ddc89d50e2c5/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:8293f3dea7fc929ef7240796ba231413afa7b68ce38fd21da2995549f5961981", size = 4927539, upload-time = "2026-02-10T19:17:40.241Z" }, - { url = "https://files.pythonhosted.org/packages/3b/02/cfe39181b02419bbbbcf3abdd16c1c5c8541f03ca8bda240debc467d5a12/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:1abfdb89b41c3be0365328a410baa9df3ff8a9110fb75e7b52e66803ddabc9a9", size = 4442199, upload-time = "2026-02-10T19:17:41.789Z" }, - { url = "https://files.pythonhosted.org/packages/c0/96/2fcaeb4873e536cf71421a388a6c11b5bc846e986b2b069c79363dc1648e/cryptography-46.0.5-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:d66e421495fdb797610a08f43b05269e0a5ea7f5e652a89bfd5a7d3c1dee3648", size = 3960131, upload-time = "2026-02-10T19:17:43.379Z" }, - { url = "https://files.pythonhosted.org/packages/d8/d2/b27631f401ddd644e94c5cf33c9a4069f72011821cf3dc7309546b0642a0/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:4e817a8920bfbcff8940ecfd60f23d01836408242b30f1a708d93198393a80b4", size = 4270072, upload-time = "2026-02-10T19:17:45.481Z" }, - { url = "https://files.pythonhosted.org/packages/f4/a7/60d32b0370dae0b4ebe55ffa10e8599a2a59935b5ece1b9f06edb73abdeb/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:68f68d13f2e1cb95163fa3b4db4bf9a159a418f5f6e7242564fc75fcae667fd0", size = 4892170, upload-time = "2026-02-10T19:17:46.997Z" }, - { url = "https://files.pythonhosted.org/packages/d2/b9/cf73ddf8ef1164330eb0b199a589103c363afa0cf794218c24d524a58eab/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a3d1fae9863299076f05cb8a778c467578262fae09f9dc0ee9b12eb4268ce663", size = 4441741, upload-time = "2026-02-10T19:17:48.661Z" }, - { url = "https://files.pythonhosted.org/packages/5f/eb/eee00b28c84c726fe8fa0158c65afe312d9c3b78d9d01daf700f1f6e37ff/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4143987a42a2397f2fc3b4d7e3a7d313fbe684f67ff443999e803dd75a76826", size = 4396728, upload-time = "2026-02-10T19:17:50.058Z" }, - { url = "https://files.pythonhosted.org/packages/65/f4/6bc1a9ed5aef7145045114b75b77c2a8261b4d38717bd8dea111a63c3442/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7d731d4b107030987fd61a7f8ab512b25b53cef8f233a97379ede116f30eb67d", size = 4652001, upload-time = "2026-02-10T19:17:51.54Z" }, - { url = "https://files.pythonhosted.org/packages/86/ef/5d00ef966ddd71ac2e6951d278884a84a40ffbd88948ef0e294b214ae9e4/cryptography-46.0.5-cp314-cp314t-win32.whl", hash = "sha256:c3bcce8521d785d510b2aad26ae2c966092b7daa8f45dd8f44734a104dc0bc1a", size = 3003637, upload-time = "2026-02-10T19:17:52.997Z" }, - { url = "https://files.pythonhosted.org/packages/b7/57/f3f4160123da6d098db78350fdfd9705057aad21de7388eacb2401dceab9/cryptography-46.0.5-cp314-cp314t-win_amd64.whl", hash = "sha256:4d8ae8659ab18c65ced284993c2265910f6c9e650189d4e3f68445ef82a810e4", size = 3469487, upload-time = "2026-02-10T19:17:54.549Z" }, - { url = "https://files.pythonhosted.org/packages/e2/fa/a66aa722105ad6a458bebd64086ca2b72cdd361fed31763d20390f6f1389/cryptography-46.0.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:4108d4c09fbbf2789d0c926eb4152ae1760d5a2d97612b92d508d96c861e4d31", size = 7170514, upload-time = "2026-02-10T19:17:56.267Z" }, - { url = "https://files.pythonhosted.org/packages/0f/04/c85bdeab78c8bc77b701bf0d9bdcf514c044e18a46dcff330df5448631b0/cryptography-46.0.5-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1f30a86d2757199cb2d56e48cce14deddf1f9c95f1ef1b64ee91ea43fe2e18", size = 4275349, upload-time = "2026-02-10T19:17:58.419Z" }, - { url = "https://files.pythonhosted.org/packages/5c/32/9b87132a2f91ee7f5223b091dc963055503e9b442c98fc0b8a5ca765fab0/cryptography-46.0.5-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:039917b0dc418bb9f6edce8a906572d69e74bd330b0b3fea4f79dab7f8ddd235", size = 4420667, upload-time = "2026-02-10T19:18:00.619Z" }, - { url = "https://files.pythonhosted.org/packages/a1/a6/a7cb7010bec4b7c5692ca6f024150371b295ee1c108bdc1c400e4c44562b/cryptography-46.0.5-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ba2a27ff02f48193fc4daeadf8ad2590516fa3d0adeeb34336b96f7fa64c1e3a", size = 4276980, upload-time = "2026-02-10T19:18:02.379Z" }, - { url = "https://files.pythonhosted.org/packages/8e/7c/c4f45e0eeff9b91e3f12dbd0e165fcf2a38847288fcfd889deea99fb7b6d/cryptography-46.0.5-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:61aa400dce22cb001a98014f647dc21cda08f7915ceb95df0c9eaf84b4b6af76", size = 4939143, upload-time = "2026-02-10T19:18:03.964Z" }, - { url = "https://files.pythonhosted.org/packages/37/19/e1b8f964a834eddb44fa1b9a9976f4e414cbb7aa62809b6760c8803d22d1/cryptography-46.0.5-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ce58ba46e1bc2aac4f7d9290223cead56743fa6ab94a5d53292ffaac6a91614", size = 4453674, upload-time = "2026-02-10T19:18:05.588Z" }, - { url = "https://files.pythonhosted.org/packages/db/ed/db15d3956f65264ca204625597c410d420e26530c4e2943e05a0d2f24d51/cryptography-46.0.5-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:420d0e909050490d04359e7fdb5ed7e667ca5c3c402b809ae2563d7e66a92229", size = 3978801, upload-time = "2026-02-10T19:18:07.167Z" }, - { url = "https://files.pythonhosted.org/packages/41/e2/df40a31d82df0a70a0daf69791f91dbb70e47644c58581d654879b382d11/cryptography-46.0.5-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:582f5fcd2afa31622f317f80426a027f30dc792e9c80ffee87b993200ea115f1", size = 4276755, upload-time = "2026-02-10T19:18:09.813Z" }, - { url = "https://files.pythonhosted.org/packages/33/45/726809d1176959f4a896b86907b98ff4391a8aa29c0aaaf9450a8a10630e/cryptography-46.0.5-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:bfd56bb4b37ed4f330b82402f6f435845a5f5648edf1ad497da51a8452d5d62d", size = 4901539, upload-time = "2026-02-10T19:18:11.263Z" }, - { url = "https://files.pythonhosted.org/packages/99/0f/a3076874e9c88ecb2ecc31382f6e7c21b428ede6f55aafa1aa272613e3cd/cryptography-46.0.5-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:a3d507bb6a513ca96ba84443226af944b0f7f47dcc9a399d110cd6146481d24c", size = 4452794, upload-time = "2026-02-10T19:18:12.914Z" }, - { url = "https://files.pythonhosted.org/packages/02/ef/ffeb542d3683d24194a38f66ca17c0a4b8bf10631feef44a7ef64e631b1a/cryptography-46.0.5-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9f16fbdf4da055efb21c22d81b89f155f02ba420558db21288b3d0035bafd5f4", size = 4404160, upload-time = "2026-02-10T19:18:14.375Z" }, - { url = "https://files.pythonhosted.org/packages/96/93/682d2b43c1d5f1406ed048f377c0fc9fc8f7b0447a478d5c65ab3d3a66eb/cryptography-46.0.5-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ced80795227d70549a411a4ab66e8ce307899fad2220ce5ab2f296e687eacde9", size = 4667123, upload-time = "2026-02-10T19:18:15.886Z" }, - { url = "https://files.pythonhosted.org/packages/45/2d/9c5f2926cb5300a8eefc3f4f0b3f3df39db7f7ce40c8365444c49363cbda/cryptography-46.0.5-cp38-abi3-win32.whl", hash = "sha256:02f547fce831f5096c9a567fd41bc12ca8f11df260959ecc7c3202555cc47a72", size = 3010220, upload-time = "2026-02-10T19:18:17.361Z" }, - { url = "https://files.pythonhosted.org/packages/48/ef/0c2f4a8e31018a986949d34a01115dd057bf536905dca38897bacd21fac3/cryptography-46.0.5-cp38-abi3-win_amd64.whl", hash = "sha256:556e106ee01aa13484ce9b0239bca667be5004efb0aabbed28d353df86445595", size = 3467050, upload-time = "2026-02-10T19:18:18.899Z" }, - { url = "https://files.pythonhosted.org/packages/eb/dd/2d9fdb07cebdf3d51179730afb7d5e576153c6744c3ff8fded23030c204e/cryptography-46.0.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:3b4995dc971c9fb83c25aa44cf45f02ba86f71ee600d81091c2f0cbae116b06c", size = 3476964, upload-time = "2026-02-10T19:18:20.687Z" }, - { url = "https://files.pythonhosted.org/packages/e9/6f/6cc6cc9955caa6eaf83660b0da2b077c7fe8ff9950a3c5e45d605038d439/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:bc84e875994c3b445871ea7181d424588171efec3e185dced958dad9e001950a", size = 4218321, upload-time = "2026-02-10T19:18:22.349Z" }, - { url = "https://files.pythonhosted.org/packages/3e/5d/c4da701939eeee699566a6c1367427ab91a8b7088cc2328c09dbee940415/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:2ae6971afd6246710480e3f15824ed3029a60fc16991db250034efd0b9fb4356", size = 4381786, upload-time = "2026-02-10T19:18:24.529Z" }, - { url = "https://files.pythonhosted.org/packages/ac/97/a538654732974a94ff96c1db621fa464f455c02d4bb7d2652f4edc21d600/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d861ee9e76ace6cf36a6a89b959ec08e7bc2493ee39d07ffe5acb23ef46d27da", size = 4217990, upload-time = "2026-02-10T19:18:25.957Z" }, - { url = "https://files.pythonhosted.org/packages/ae/11/7e500d2dd3ba891197b9efd2da5454b74336d64a7cc419aa7327ab74e5f6/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:2b7a67c9cd56372f3249b39699f2ad479f6991e62ea15800973b956f4b73e257", size = 4381252, upload-time = "2026-02-10T19:18:27.496Z" }, - { url = "https://files.pythonhosted.org/packages/bc/58/6b3d24e6b9bc474a2dcdee65dfd1f008867015408a271562e4b690561a4d/cryptography-46.0.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8456928655f856c6e1533ff59d5be76578a7157224dbd9ce6872f25055ab9ab7", size = 3407605, upload-time = "2026-02-10T19:18:29.233Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/a4/ba/04b1bd4218cbc58dc90ce967106d51582371b898690f3ae0402876cc4f34/cryptography-46.0.6.tar.gz", hash = "sha256:27550628a518c5c6c903d84f637fbecf287f6cb9ced3804838a1295dc1fd0759", size = 750542, upload-time = "2026-03-25T23:34:53.396Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/23/9285e15e3bc57325b0a72e592921983a701efc1ee8f91c06c5f0235d86d9/cryptography-46.0.6-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:64235194bad039a10bb6d2d930ab3323baaec67e2ce36215fd0952fad0930ca8", size = 7176401, upload-time = "2026-03-25T23:33:22.096Z" }, + { url = "https://files.pythonhosted.org/packages/60/f8/e61f8f13950ab6195b31913b42d39f0f9afc7d93f76710f299b5ec286ae6/cryptography-46.0.6-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:26031f1e5ca62fcb9d1fcb34b2b60b390d1aacaa15dc8b895a9ed00968b97b30", size = 4275275, upload-time = "2026-03-25T23:33:23.844Z" }, + { url = "https://files.pythonhosted.org/packages/19/69/732a736d12c2631e140be2348b4ad3d226302df63ef64d30dfdb8db7ad1c/cryptography-46.0.6-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9a693028b9cbe51b5a1136232ee8f2bc242e4e19d456ded3fa7c86e43c713b4a", size = 4425320, upload-time = "2026-03-25T23:33:25.703Z" }, + { url = "https://files.pythonhosted.org/packages/d4/12/123be7292674abf76b21ac1fc0e1af50661f0e5b8f0ec8285faac18eb99e/cryptography-46.0.6-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:67177e8a9f421aa2d3a170c3e56eca4e0128883cf52a071a7cbf53297f18b175", size = 4278082, upload-time = "2026-03-25T23:33:27.423Z" }, + { url = "https://files.pythonhosted.org/packages/5b/ba/d5e27f8d68c24951b0a484924a84c7cdaed7502bac9f18601cd357f8b1d2/cryptography-46.0.6-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:d9528b535a6c4f8ff37847144b8986a9a143585f0540fbcb1a98115b543aa463", size = 4926514, upload-time = "2026-03-25T23:33:29.206Z" }, + { url = "https://files.pythonhosted.org/packages/34/71/1ea5a7352ae516d5512d17babe7e1b87d9db5150b21f794b1377eac1edc0/cryptography-46.0.6-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:22259338084d6ae497a19bae5d4c66b7ca1387d3264d1c2c0e72d9e9b6a77b97", size = 4457766, upload-time = "2026-03-25T23:33:30.834Z" }, + { url = "https://files.pythonhosted.org/packages/01/59/562be1e653accee4fdad92c7a2e88fced26b3fdfce144047519bbebc299e/cryptography-46.0.6-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:760997a4b950ff00d418398ad73fbc91aa2894b5c1db7ccb45b4f68b42a63b3c", size = 3986535, upload-time = "2026-03-25T23:33:33.02Z" }, + { url = "https://files.pythonhosted.org/packages/d6/8b/b1ebfeb788bf4624d36e45ed2662b8bd43a05ff62157093c1539c1288a18/cryptography-46.0.6-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:3dfa6567f2e9e4c5dceb8ccb5a708158a2a871052fa75c8b78cb0977063f1507", size = 4277618, upload-time = "2026-03-25T23:33:34.567Z" }, + { url = "https://files.pythonhosted.org/packages/dd/52/a005f8eabdb28df57c20f84c44d397a755782d6ff6d455f05baa2785bd91/cryptography-46.0.6-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:cdcd3edcbc5d55757e5f5f3d330dd00007ae463a7e7aa5bf132d1f22a4b62b19", size = 4890802, upload-time = "2026-03-25T23:33:37.034Z" }, + { url = "https://files.pythonhosted.org/packages/ec/4d/8e7d7245c79c617d08724e2efa397737715ca0ec830ecb3c91e547302555/cryptography-46.0.6-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:d4e4aadb7fc1f88687f47ca20bb7227981b03afaae69287029da08096853b738", size = 4457425, upload-time = "2026-03-25T23:33:38.904Z" }, + { url = "https://files.pythonhosted.org/packages/1d/5c/f6c3596a1430cec6f949085f0e1a970638d76f81c3ea56d93d564d04c340/cryptography-46.0.6-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2b417edbe8877cda9022dde3a008e2deb50be9c407eef034aeeb3a8b11d9db3c", size = 4405530, upload-time = "2026-03-25T23:33:40.842Z" }, + { url = "https://files.pythonhosted.org/packages/7e/c9/9f9cea13ee2dbde070424e0c4f621c091a91ffcc504ffea5e74f0e1daeff/cryptography-46.0.6-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:380343e0653b1c9d7e1f55b52aaa2dbb2fdf2730088d48c43ca1c7c0abb7cc2f", size = 4667896, upload-time = "2026-03-25T23:33:42.781Z" }, + { url = "https://files.pythonhosted.org/packages/ad/b5/1895bc0821226f129bc74d00eccfc6a5969e2028f8617c09790bf89c185e/cryptography-46.0.6-cp311-abi3-win32.whl", hash = "sha256:bcb87663e1f7b075e48c3be3ecb5f0b46c8fc50b50a97cf264e7f60242dca3f2", size = 3026348, upload-time = "2026-03-25T23:33:45.021Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f8/c9bcbf0d3e6ad288b9d9aa0b1dee04b063d19e8c4f871855a03ab3a297ab/cryptography-46.0.6-cp311-abi3-win_amd64.whl", hash = "sha256:6739d56300662c468fddb0e5e291f9b4d084bead381667b9e654c7dd81705124", size = 3483896, upload-time = "2026-03-25T23:33:46.649Z" }, + { url = "https://files.pythonhosted.org/packages/01/41/3a578f7fd5c70611c0aacba52cd13cb364a5dee895a5c1d467208a9380b0/cryptography-46.0.6-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:2ef9e69886cbb137c2aef9772c2e7138dc581fad4fcbcf13cc181eb5a3ab6275", size = 7117147, upload-time = "2026-03-25T23:33:48.249Z" }, + { url = "https://files.pythonhosted.org/packages/fa/87/887f35a6fca9dde90cad08e0de0c89263a8e59b2d2ff904fd9fcd8025b6f/cryptography-46.0.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7f417f034f91dcec1cb6c5c35b07cdbb2ef262557f701b4ecd803ee8cefed4f4", size = 4266221, upload-time = "2026-03-25T23:33:49.874Z" }, + { url = "https://files.pythonhosted.org/packages/aa/a8/0a90c4f0b0871e0e3d1ed126aed101328a8a57fd9fd17f00fb67e82a51ca/cryptography-46.0.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d24c13369e856b94892a89ddf70b332e0b70ad4a5c43cf3e9cb71d6d7ffa1f7b", size = 4408952, upload-time = "2026-03-25T23:33:52.128Z" }, + { url = "https://files.pythonhosted.org/packages/16/0b/b239701eb946523e4e9f329336e4ff32b1247e109cbab32d1a7b61da8ed7/cryptography-46.0.6-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:aad75154a7ac9039936d50cf431719a2f8d4ed3d3c277ac03f3339ded1a5e707", size = 4270141, upload-time = "2026-03-25T23:33:54.11Z" }, + { url = "https://files.pythonhosted.org/packages/0f/a8/976acdd4f0f30df7b25605f4b9d3d89295351665c2091d18224f7ad5cdbf/cryptography-46.0.6-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:3c21d92ed15e9cfc6eb64c1f5a0326db22ca9c2566ca46d845119b45b4400361", size = 4904178, upload-time = "2026-03-25T23:33:55.725Z" }, + { url = "https://files.pythonhosted.org/packages/b1/1b/bf0e01a88efd0e59679b69f42d4afd5bced8700bb5e80617b2d63a3741af/cryptography-46.0.6-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:4668298aef7cddeaf5c6ecc244c2302a2b8e40f384255505c22875eebb47888b", size = 4441812, upload-time = "2026-03-25T23:33:57.364Z" }, + { url = "https://files.pythonhosted.org/packages/bb/8b/11df86de2ea389c65aa1806f331cae145f2ed18011f30234cc10ca253de8/cryptography-46.0.6-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:8ce35b77aaf02f3b59c90b2c8a05c73bac12cea5b4e8f3fbece1f5fddea5f0ca", size = 3963923, upload-time = "2026-03-25T23:33:59.361Z" }, + { url = "https://files.pythonhosted.org/packages/91/e0/207fb177c3a9ef6a8108f234208c3e9e76a6aa8cf20d51932916bd43bda0/cryptography-46.0.6-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:c89eb37fae9216985d8734c1afd172ba4927f5a05cfd9bf0e4863c6d5465b013", size = 4269695, upload-time = "2026-03-25T23:34:00.909Z" }, + { url = "https://files.pythonhosted.org/packages/21/5e/19f3260ed1e95bced52ace7501fabcd266df67077eeb382b79c81729d2d3/cryptography-46.0.6-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:ed418c37d095aeddf5336898a132fba01091f0ac5844e3e8018506f014b6d2c4", size = 4869785, upload-time = "2026-03-25T23:34:02.796Z" }, + { url = "https://files.pythonhosted.org/packages/10/38/cd7864d79aa1d92ef6f1a584281433419b955ad5a5ba8d1eb6c872165bcb/cryptography-46.0.6-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:69cf0056d6947edc6e6760e5f17afe4bea06b56a9ac8a06de9d2bd6b532d4f3a", size = 4441404, upload-time = "2026-03-25T23:34:04.35Z" }, + { url = "https://files.pythonhosted.org/packages/09/0a/4fe7a8d25fed74419f91835cf5829ade6408fd1963c9eae9c4bce390ecbb/cryptography-46.0.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e7304c4f4e9490e11efe56af6713983460ee0780f16c63f219984dab3af9d2d", size = 4397549, upload-time = "2026-03-25T23:34:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/5f/a0/7d738944eac6513cd60a8da98b65951f4a3b279b93479a7e8926d9cd730b/cryptography-46.0.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b928a3ca837c77a10e81a814a693f2295200adb3352395fad024559b7be7a736", size = 4651874, upload-time = "2026-03-25T23:34:07.916Z" }, + { url = "https://files.pythonhosted.org/packages/cb/f1/c2326781ca05208845efca38bf714f76939ae446cd492d7613808badedf1/cryptography-46.0.6-cp314-cp314t-win32.whl", hash = "sha256:97c8115b27e19e592a05c45d0dd89c57f81f841cc9880e353e0d3bf25b2139ed", size = 3001511, upload-time = "2026-03-25T23:34:09.892Z" }, + { url = "https://files.pythonhosted.org/packages/c9/57/fe4a23eb549ac9d903bd4698ffda13383808ef0876cc912bcb2838799ece/cryptography-46.0.6-cp314-cp314t-win_amd64.whl", hash = "sha256:c797e2517cb7880f8297e2c0f43bb910e91381339336f75d2c1c2cbf811b70b4", size = 3471692, upload-time = "2026-03-25T23:34:11.613Z" }, + { url = "https://files.pythonhosted.org/packages/c4/cc/f330e982852403da79008552de9906804568ae9230da8432f7496ce02b71/cryptography-46.0.6-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:12cae594e9473bca1a7aceb90536060643128bb274fcea0fc459ab90f7d1ae7a", size = 7162776, upload-time = "2026-03-25T23:34:13.308Z" }, + { url = "https://files.pythonhosted.org/packages/49/b3/dc27efd8dcc4bff583b3f01d4a3943cd8b5821777a58b3a6a5f054d61b79/cryptography-46.0.6-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:639301950939d844a9e1c4464d7e07f902fe9a7f6b215bb0d4f28584729935d8", size = 4270529, upload-time = "2026-03-25T23:34:15.019Z" }, + { url = "https://files.pythonhosted.org/packages/e6/05/e8d0e6eb4f0d83365b3cb0e00eb3c484f7348db0266652ccd84632a3d58d/cryptography-46.0.6-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ed3775295fb91f70b4027aeba878d79b3e55c0b3e97eaa4de71f8f23a9f2eb77", size = 4414827, upload-time = "2026-03-25T23:34:16.604Z" }, + { url = "https://files.pythonhosted.org/packages/2f/97/daba0f5d2dc6d855e2dcb70733c812558a7977a55dd4a6722756628c44d1/cryptography-46.0.6-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:8927ccfbe967c7df312ade694f987e7e9e22b2425976ddbf28271d7e58845290", size = 4271265, upload-time = "2026-03-25T23:34:18.586Z" }, + { url = "https://files.pythonhosted.org/packages/89/06/fe1fce39a37ac452e58d04b43b0855261dac320a2ebf8f5260dd55b201a9/cryptography-46.0.6-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:b12c6b1e1651e42ab5de8b1e00dc3b6354fdfd778e7fa60541ddacc27cd21410", size = 4916800, upload-time = "2026-03-25T23:34:20.561Z" }, + { url = "https://files.pythonhosted.org/packages/ff/8a/b14f3101fe9c3592603339eb5d94046c3ce5f7fc76d6512a2d40efd9724e/cryptography-46.0.6-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:063b67749f338ca9c5a0b7fe438a52c25f9526b851e24e6c9310e7195aad3b4d", size = 4448771, upload-time = "2026-03-25T23:34:22.406Z" }, + { url = "https://files.pythonhosted.org/packages/01/b3/0796998056a66d1973fd52ee89dc1bb3b6581960a91ad4ac705f182d398f/cryptography-46.0.6-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:02fad249cb0e090b574e30b276a3da6a149e04ee2f049725b1f69e7b8351ec70", size = 3978333, upload-time = "2026-03-25T23:34:24.281Z" }, + { url = "https://files.pythonhosted.org/packages/c5/3d/db200af5a4ffd08918cd55c08399dc6c9c50b0bc72c00a3246e099d3a849/cryptography-46.0.6-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e6142674f2a9291463e5e150090b95a8519b2fb6e6aaec8917dd8d094ce750d", size = 4271069, upload-time = "2026-03-25T23:34:25.895Z" }, + { url = "https://files.pythonhosted.org/packages/d7/18/61acfd5b414309d74ee838be321c636fe71815436f53c9f0334bf19064fa/cryptography-46.0.6-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:456b3215172aeefb9284550b162801d62f5f264a081049a3e94307fe20792cfa", size = 4878358, upload-time = "2026-03-25T23:34:27.67Z" }, + { url = "https://files.pythonhosted.org/packages/8b/65/5bf43286d566f8171917cae23ac6add941654ccf085d739195a4eacf1674/cryptography-46.0.6-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:341359d6c9e68834e204ceaf25936dffeafea3829ab80e9503860dcc4f4dac58", size = 4448061, upload-time = "2026-03-25T23:34:29.375Z" }, + { url = "https://files.pythonhosted.org/packages/e0/25/7e49c0fa7205cf3597e525d156a6bce5b5c9de1fd7e8cb01120e459f205a/cryptography-46.0.6-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9a9c42a2723999a710445bc0d974e345c32adfd8d2fac6d8a251fa829ad31cfb", size = 4399103, upload-time = "2026-03-25T23:34:32.036Z" }, + { url = "https://files.pythonhosted.org/packages/44/46/466269e833f1c4718d6cd496ffe20c56c9c8d013486ff66b4f69c302a68d/cryptography-46.0.6-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6617f67b1606dfd9fe4dbfa354a9508d4a6d37afe30306fe6c101b7ce3274b72", size = 4659255, upload-time = "2026-03-25T23:34:33.679Z" }, + { url = "https://files.pythonhosted.org/packages/0a/09/ddc5f630cc32287d2c953fc5d32705e63ec73e37308e5120955316f53827/cryptography-46.0.6-cp38-abi3-win32.whl", hash = "sha256:7f6690b6c55e9c5332c0b59b9c8a3fb232ebf059094c17f9019a51e9827df91c", size = 3010660, upload-time = "2026-03-25T23:34:35.418Z" }, + { url = "https://files.pythonhosted.org/packages/1b/82/ca4893968aeb2709aacfb57a30dec6fa2ab25b10fa9f064b8882ce33f599/cryptography-46.0.6-cp38-abi3-win_amd64.whl", hash = "sha256:79e865c642cfc5c0b3eb12af83c35c5aeff4fa5c672dc28c43721c2c9fdd2f0f", size = 3471160, upload-time = "2026-03-25T23:34:37.191Z" }, + { url = "https://files.pythonhosted.org/packages/2e/84/7ccff00ced5bac74b775ce0beb7d1be4e8637536b522b5df9b73ada42da2/cryptography-46.0.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:2ea0f37e9a9cf0df2952893ad145fd9627d326a59daec9b0802480fa3bcd2ead", size = 3475444, upload-time = "2026-03-25T23:34:38.944Z" }, + { url = "https://files.pythonhosted.org/packages/bc/1f/4c926f50df7749f000f20eede0c896769509895e2648db5da0ed55db711d/cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a3e84d5ec9ba01f8fd03802b2147ba77f0c8f2617b2aff254cedd551844209c8", size = 4218227, upload-time = "2026-03-25T23:34:40.871Z" }, + { url = "https://files.pythonhosted.org/packages/c6/65/707be3ffbd5f786028665c3223e86e11c4cda86023adbc56bd72b1b6bab5/cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:12f0fa16cc247b13c43d56d7b35287ff1569b5b1f4c5e87e92cc4fcc00cd10c0", size = 4381399, upload-time = "2026-03-25T23:34:42.609Z" }, + { url = "https://files.pythonhosted.org/packages/f3/6d/73557ed0ef7d73d04d9aba745d2c8e95218213687ee5e76b7d236a5030fc/cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:50575a76e2951fe7dbd1f56d181f8c5ceeeb075e9ff88e7ad997d2f42af06e7b", size = 4217595, upload-time = "2026-03-25T23:34:44.205Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c5/e1594c4eec66a567c3ac4400008108a415808be2ce13dcb9a9045c92f1a0/cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:90e5f0a7b3be5f40c3a0a0eafb32c681d8d2c181fc2a1bdabe9b3f611d9f6b1a", size = 4380912, upload-time = "2026-03-25T23:34:46.328Z" }, + { url = "https://files.pythonhosted.org/packages/1a/89/843b53614b47f97fe1abc13f9a86efa5ec9e275292c457af1d4a60dc80e0/cryptography-46.0.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6728c49e3b2c180ef26f8e9f0a883a2c585638db64cf265b49c9ba10652d430e", size = 3409955, upload-time = "2026-03-25T23:34:48.465Z" }, ] [[package]]