diff --git a/docs/ADMIN_INITIALIZATION_FIX.md b/docs/ADMIN_INITIALIZATION_FIX.md index 7b905a19a..888494e28 100644 --- a/docs/ADMIN_INITIALIZATION_FIX.md +++ b/docs/ADMIN_INITIALIZATION_FIX.md @@ -125,7 +125,7 @@ async fn ensure_default_admin_exists( ### 1. 管理员登录 ```bash -curl -X POST http://127.0.0.1:8080/admin/login \ +curl -X POST http://127.0.0.1:8080/api/admin/login \ -H "Content-Type: application/json" \ -d '{"password":"admin"}' ``` @@ -151,7 +151,7 @@ cpr_admin_session=sess_a27875537483420ab7ba6ce4d1f91b5e; Path=/; HttpOnly; SameS ### 2. 导入测试账号 ```bash -curl -X POST http://127.0.0.1:8080/admin/accounts/import \ +curl -X POST http://127.0.0.1:8080/api/admin/accounts/import \ -H "Content-Type: application/json" \ -H "Cookie: cpr_admin_session=sess_a27875537483420ab7ba6ce4d1f91b5e" \ -d @accounts-export.json @@ -177,7 +177,7 @@ curl -X POST http://127.0.0.1:8080/admin/accounts/import \ #### 3.1 创建 API 密钥 ```bash -curl -X POST http://127.0.0.1:8080/admin/api-keys \ +curl -X POST http://127.0.0.1:8080/api/admin/api-keys \ -H "Content-Type: application/json" \ -H "Cookie: cpr_admin_session=sess_..." \ -d '{"name":"test-key","label":"测试密钥"}' @@ -197,7 +197,7 @@ curl -X POST http://127.0.0.1:8080/admin/api-keys \ #### 3.2 刷新模型列表 ```bash -curl -X POST http://127.0.0.1:8080/admin/refresh-models \ +curl -X POST http://127.0.0.1:8080/api/admin/refresh-models \ -H "Cookie: cpr_admin_session=sess_..." ``` @@ -262,7 +262,7 @@ curl -X POST http://127.0.0.1:8080/v1/chat/completions \ 由于 `AdminConfig` 添加了新字段,需要修复所有测试文件中的初始化代码。 **影响的测试文件:** -- `tests/admin/*.rs` (6 个文件) +- `tests/api/admin/*.rs` (6 个文件) - `tests/codex_serving/*.rs` (2 个文件) - `tests/runtime/startup.rs` - `tests/support/*.rs` (2 个文件) @@ -325,7 +325,7 @@ test result: ok. 49 passed; 0 failed - `tests/**/*.rs` - 修复测试中的 AdminConfig 初始化 ### 不变的模块 -- `src/admin/auth/service.rs` - 认证逻辑无需变更 +- `src/api/admin/auth/service.rs` - 认证逻辑无需变更 - `src/platform/identity/admin_session.rs` - 密码哈希逻辑无需变更 - `src/platform/storage/schema.sql` - 数据库表结构无需变更 diff --git a/docs/FINGERPRINT_FIX.md b/docs/FINGERPRINT_FIX.md index df1d59822..7959e25f9 100644 --- a/docs/FINGERPRINT_FIX.md +++ b/docs/FINGERPRINT_FIX.md @@ -203,7 +203,7 @@ pub fn capture_set_cookie(&mut self, account_id: &str, raw: &str) { - 添加所有浏览器特征头 - 修正 `x-openai-internal-codex-residency` 为 `"us"` -4. **src/codex/gateway/transport/client.rs** +4. **src/codex/gateway/transport/http_client.rs** - 更新导入:`build_codex_headers` → `build_ordered_codex_headers` - 重构 `request_headers()` 使用排序后的头 diff --git a/docs/IMPLEMENTATION_COMPLETE.md b/docs/IMPLEMENTATION_COMPLETE.md index 3c4e8c611..badd087ec 100644 --- a/docs/IMPLEMENTATION_COMPLETE.md +++ b/docs/IMPLEMENTATION_COMPLETE.md @@ -24,7 +24,7 @@ ### 2. 核心传输层更新 -#### `src/codex/gateway/transport/client.rs` +#### `src/codex/gateway/transport/http_client.rs` - `CodexRequestContext` 添加两个新字段: - `installation_id: Option<&'a str>` - `session_id: Option<&'a str>` @@ -209,10 +209,10 @@ Dispatch Service (src/codex/serving/dispatch/mod.rs) ### 修改文件 (9) - `src/codex/gateway/mod.rs` (+2 行) -- `src/codex/gateway/transport/client.rs` (+4 行) +- `src/codex/gateway/transport/http_client.rs` (+4 行) - `src/codex/serving/dispatch/mod.rs` (+15 行) - `src/codex/accounts/service/health.rs` (+2 行) -- `src/codex/accounts/models/service.rs` (+2 行) +- `src/codex/models/service.rs` (+2 行) - `src/codex/serving/diagnostics.rs` (+2 行) - `Cargo.toml` (+1 行) - `tests/codex_gateway/client.rs` (+2 行) diff --git a/docs/OPENAI_CODEX_PARITY_AUDIT.md b/docs/OPENAI_CODEX_PARITY_AUDIT.md index bb54ce4a3..61701bd71 100644 --- a/docs/OPENAI_CODEX_PARITY_AUDIT.md +++ b/docs/OPENAI_CODEX_PARITY_AUDIT.md @@ -17,7 +17,7 @@ Rust 状态: - 到位:`src/codex/gateway/transport/headers.rs` 构造账号、residency、request id、UA、sec-ch、默认 header 和顺序。 -- 到位:`src/codex/gateway/transport/client.rs` 将 installation/window/turn/parent/subagent 同步到 header 和 `client_metadata`。 +- 到位:`src/codex/gateway/transport/http_client.rs` 将 installation/window/turn/parent/subagent 同步到 header 和 `client_metadata`。 - 到位:`src/codex/gateway/installation.rs` 兼容读取 `~/.codex/installation_id`,否则落库生成。 - 到位:`src/runtime/bootstrap.rs` 从数据库加载自动更新指纹,避免实际请求继续使用硬编码默认值。 - 边界:Rust 使用 `reqwest + rustls` 传输,未声称复制原版 native transport 的完整 Chrome TLS 指纹。IP 代理/VPN 相关能力不属于移植范围。 diff --git a/docs/OPENAI_REQUEST_ALIGNMENT.md b/docs/OPENAI_REQUEST_ALIGNMENT.md index 3f939bd0b..01413aeb2 100644 --- a/docs/OPENAI_REQUEST_ALIGNMENT.md +++ b/docs/OPENAI_REQUEST_ALIGNMENT.md @@ -28,7 +28,7 @@ - 使用 SHA256 哈希生成确定性标识符 - 格式:`cp_{hash[..32]}` (conversation) 或 `cw_{hash[..32]}` (window) -- 更新 `src/codex/gateway/transport/client.rs` +- 更新 `src/codex/gateway/transport/http_client.rs` - 在 `CodexRequestContext` 添加 `installation_id` 和 `session_id` 字段 - 在 `request_headers()` 方法中添加这两个 header @@ -132,10 +132,10 @@ test codex::gateway::installation::tests::test_generate_and_persist ... ok ### 修改文件 1. `src/codex/gateway/mod.rs` - 导出新模块 -2. `src/codex/gateway/transport/client.rs` - 添加新 header 字段 +2. `src/codex/gateway/transport/http_client.rs` - 添加新 header 字段 3. `src/codex/serving/dispatch/mod.rs` - 集成身份派生逻辑 4. `src/codex/accounts/service/health.rs` - 更新 context 构造 -5. `src/codex/accounts/models/service.rs` - 更新 context 构造 +5. `src/codex/models/service.rs` - 更新 context 构造 6. `src/codex/serving/diagnostics.rs` - 更新 context 构造 7. `Cargo.toml` - 添加 `dirs = "5.0.1"` 依赖 diff --git a/docs/api.md b/docs/api.md index e80014ee3..a04771c4e 100644 --- a/docs/api.md +++ b/docs/api.md @@ -1,10 +1,12 @@ # API Contract -`codex-proxy-rs` exposes two API families. +`codex-proxy-rs` exposes two API families. The `/admin/*` path is reserved for +the future human Web console; machine-readable management endpoints live under +`/api/admin/*`. ## `/v1/*` -These endpoints are OpenAI-compatible and are authenticated only by enabled local client API keys created through `/admin/api-keys` and sent as `Authorization: Bearer cpr_...`. +These endpoints are OpenAI-compatible and are authenticated only by enabled local client API keys created through `/api/admin/api-keys` and sent as `Authorization: Bearer cpr_...`. Responses include an `X-Request-Id` header for tracing, but the body stays OpenAI-compatible and does not include a custom `requestId` field. Error body: @@ -159,14 +161,14 @@ Refresh failures are recorded as non-secret `account.refresh` event logs with th `previous_response_id` and explicit WebSocket-only requests are rejected until the WebSocket transport is implemented and verified. -## `/admin/*` +## `/api/admin/*` Admin endpoints are authenticated only by HttpOnly admin session cookies. Admin JSON uses lower camelCase field names. Every admin response includes an `X-Request-Id` header, and every JSON body includes `requestId`. Use real HTTP status codes and body-level frontend codes together. Do not return HTTP `200` for failed requests. The body `code` exists for frontend branching; the HTTP status remains the transport truth. -### `GET /admin/diagnostics` +### `GET /api/admin/diagnostics` Returns the same non-secret runtime diagnostics summary as `/debug/diagnostics`, wrapped in the admin envelope and gated by the admin session cookie. @@ -193,7 +195,7 @@ Success response: } ``` -### `POST /admin/login` +### `POST /api/admin/login` Authenticates only with the configured admin password stored in `admin_users`. Client API keys (`Bearer cpr_...`) are ignored by the admin login flow and cannot create admin sessions. @@ -226,7 +228,7 @@ Set-Cookie: cpr_admin_session=...; Path=/; HttpOnly; SameSite=Lax; Max-Age=3600 Invalid password response uses HTTP `401` and body code `40102`. -### `GET /admin/settings` +### `GET /api/admin/settings` Returns the in-scope runtime settings visible to the admin UI. This endpoint uses admin sessions, not client API keys. @@ -263,7 +265,7 @@ Success response: } ``` -### `PATCH /admin/settings` +### `PATCH /api/admin/settings` Updates retained settings and writes them to root `local.yaml` as a config overlay. The route requires the admin session cookie. Client API keys and removed TypeScript-era fields are not accepted. @@ -297,7 +299,7 @@ Request: } ``` -Success response uses the same `data` shape as `GET /admin/settings`. +Success response uses the same `data` shape as `GET /api/admin/settings`. Validation: @@ -306,7 +308,7 @@ Validation: - Quota warning thresholds must be between `0` and `100`. - Removed settings such as `proxyUrl`, `openaiApiKey`, `ollama`, provider settings, proxy API key settings, and Electron/self-update settings return HTTP `400` with body code `40001`. -### `GET /admin/logs` +### `GET /api/admin/logs` Returns cursor-paginated event logs for admin troubleshooting. The list uses the standard admin page envelope and lower camelCase fields. @@ -348,7 +350,7 @@ Example item: } ``` -### `GET /admin/logs/state` +### `GET /api/admin/logs/state` Returns the current retained logging configuration plus the number of stored SQLite event logs. @@ -368,9 +370,9 @@ Success response: } ``` -### `PATCH /admin/logs/state` +### `PATCH /api/admin/logs/state` -Updates the in-process logging state used by admin log controls. This is runtime-only; persistent configuration changes still go through `PATCH /admin/settings`. +Updates the in-process logging state used by admin log controls. This is runtime-only; persistent configuration changes still go through `PATCH /api/admin/settings`. Request: @@ -382,13 +384,13 @@ Request: } ``` -Success response uses the same data shape as `GET /admin/logs/state`. +Success response uses the same data shape as `GET /api/admin/logs/state`. -### `GET /admin/logs/{id}` +### `GET /api/admin/logs/{id}` Returns a single stored event log by id using the same event shape as list items. Missing ids return HTTP `404` with body code `40401`. -### `DELETE /admin/logs` +### `DELETE /api/admin/logs` Clears stored SQLite event logs. @@ -405,7 +407,7 @@ Success response: } ``` -### `GET /admin/usage-stats` +### `GET /api/admin/usage-stats` Returns cursor-paginated account usage counters recorded after `/v1/*` calls. The endpoint never returns account access tokens, refresh tokens, Cookie values, or client API keys. @@ -425,7 +427,7 @@ Example item: } ``` -### `GET /admin/usage-stats/summary` +### `GET /api/admin/usage-stats/summary` Returns global usage totals across accounts. @@ -446,11 +448,11 @@ Success response: } ``` -### `GET /admin/api-keys` +### `GET /api/admin/api-keys` Returns local client API keys with cursor pagination. The plaintext key and hash are never returned by list endpoints. -### `POST /admin/api-keys` +### `POST /api/admin/api-keys` Creates a local client API key for `/v1/*`. The plaintext value is returned only in this response; store it client-side and use it as `Authorization: Bearer `. @@ -481,14 +483,14 @@ Success response: } ``` -### `GET /admin/api-keys/export` +### `GET /api/admin/api-keys/export` Exports Rust-local client API key metadata for backup or migration. The export never includes plaintext keys, key hashes, or the raw auth pepper. Because `cpr_` keys are stored only as peppered HMAC hashes, imports rotate keys instead of preserving the original plaintext. Optional query: ```http -GET /admin/api-keys/export?ids=key_01,key_02 +GET /api/admin/api-keys/export?ids=key_01,key_02 ``` Success response: @@ -516,7 +518,7 @@ Success response: } ``` -### `POST /admin/api-keys/import` +### `POST /api/admin/api-keys/import` Imports Rust-local client API key metadata exported by this service. Import creates new local `cpr_` keys using the target instance's pepper and returns the new plaintext values only in this response. Clients must replace the old keys with these rotated plaintext values. @@ -567,11 +569,11 @@ Success response: } ``` -### `GET /admin/accounts` +### `GET /api/admin/accounts` Returns stored Codex accounts with cursor pagination. Access tokens and refresh tokens are never decrypted or returned. -### `GET /admin/accounts/quota-warnings` +### `GET /api/admin/accounts/quota-warnings` Returns active quota warnings computed from cached account quota snapshots in SQLite. The route requires an admin session cookie and does not call the upstream Codex backend. @@ -598,7 +600,7 @@ Success response: } ``` -### `POST /admin/accounts/import` +### `POST /api/admin/accounts/import` Imports accounts into encrypted SQLite storage. The request body uses the Rust import format, which accepts the exported account object fields needed by this service and ignores unrelated fields. diff --git a/docs/architecture-audit.md b/docs/architecture-audit.md index 4c1888a5d..8eaa8eccd 100644 --- a/docs/architecture-audit.md +++ b/docs/architecture-audit.md @@ -13,7 +13,7 @@ src/ ├── codex/ # OpenAI/Codex 对接层(核心业务逻辑) ├── service/ # 业务服务层(Chat, Responses, Diagnostics) -├── http/ # HTTP 路由层(/v1/* 和 /admin/*) +├── http/ # HTTP 路由层(/v1/* 和 /api/admin/*) ├── auth/ # 认证模块(API keys, sessions) ├── scheduler/ # 后台调度器(refresh, quota, model) ├── storage/ # 数据持久化(SQLite) @@ -30,7 +30,7 @@ src/ **三层认证边界设计:** ``` -Admin Auth (密码 + Session) → /admin/* +Admin Auth (密码 + Session) → /api/admin/* Client API Keys (cpr_ 前缀) → /v1/* Upstream Tokens (内部加密) → ChatGPT Backend ``` @@ -151,7 +151,7 @@ src/codex/accounts/service/ ├── cookies.rs # Cookie 操作 ├── health.rs # 健康检查 ├── import.rs # 导入逻辑 -├── mutation.rs # CRUD 操作 +├── lifecycle.rs # CRUD 操作 ├── quota.rs # 配额管理 ├── refresh.rs # 令牌刷新 └── runtime_pool.rs # 运行时池同步 @@ -224,7 +224,7 @@ pub enum AccountServiceError { ### 5. 🟢 配置热重载未完全实现 **当前状态:** -- `/admin/settings PATCH` 写入 `local.yaml` +- `/api/admin/settings PATCH` 写入 `local.yaml` - 更新 `AppState` 中的 config - ⚠️ 但已构造的服务(如调度器)仍使用旧配置 diff --git a/docs/architecture-capability-plan.md b/docs/architecture-capability-plan.md index 86e5ac012..b01c21611 100644 --- a/docs/architecture-capability-plan.md +++ b/docs/architecture-capability-plan.md @@ -75,7 +75,7 @@ src/ │ │ │ ├── cookies.rs │ │ │ ├── health.rs │ │ │ ├── import.rs # 手动/CLI/OAuth 导入 -│ │ │ ├── mutation.rs +│ │ │ ├── lifecycle.rs │ │ │ ├── quota.rs │ │ │ ├── refresh.rs │ │ │ └── runtime_pool.rs @@ -136,18 +136,18 @@ src/ │ ├── admin/ # 🔵 后台管理域(控制面) │ ├── mod.rs -│ ├── http/ # /admin/* ← http/admin/ -│ │ ├── mod.rs # ← http/admin/mod.rs -│ │ ├── router.rs # ← http/admin/router.rs -│ │ ├── response.rs # ← http/admin/response.rs(AdminError envelope) -│ │ ├── auth.rs # ← http/admin/auth.rs(login/status/logout) -│ │ ├── api_keys.rs # ← http/admin/api_keys.rs -│ │ ├── logs.rs # ← http/admin/logs.rs -│ │ ├── models.rs # ← http/admin/models.rs(refresh-models) -│ │ ├── settings.rs # ← http/admin/settings.rs -│ │ ├── usage.rs # ← http/admin/usage.rs -│ │ ├── diagnostics.rs # ← http/admin/diagnostics.rs -│ │ └── accounts/ # ⭐ 拆分 http/admin/accounts.rs(1697 行) +│ ├── http/ # /api/admin/* ← http/api/admin/ +│ │ ├── mod.rs # ← http/api/admin/mod.rs +│ │ ├── router.rs # ← http/api/admin/router.rs +│ │ ├── response.rs # ← http/api/admin/response.rs(AdminError envelope) +│ │ ├── auth.rs # ← http/api/admin/auth.rs(login/status/logout) +│ │ ├── api_keys.rs # ← http/api/admin/api_keys.rs +│ │ ├── logs.rs # ← http/api/admin/logs.rs +│ │ ├── models.rs # ← http/api/admin/models.rs(refresh-models) +│ │ ├── settings.rs # ← http/api/admin/settings.rs +│ │ ├── usage.rs # ← http/api/admin/usage.rs +│ │ ├── diagnostics.rs # ← http/api/admin/diagnostics.rs +│ │ └── accounts/ # ⭐ 拆分 http/api/admin/accounts.rs(1697 行) │ │ ├── mod.rs # 路由注册 + 共享 DTO │ │ ├── list.rs # accounts / export_accounts │ │ ├── create.rs # create_account / import_accounts / import_cli_auth @@ -156,7 +156,7 @@ src/ │ │ ├── health.rs # health_check_accounts │ │ ├── quota.rs # quota_warnings / account_quota │ │ ├── cookies.rs # get / set / delete cookies -│ │ └── oauth.rs # ← http/admin/auth.rs 中 OAuth 流程 +│ │ └── oauth.rs # ← http/api/admin/auth.rs 中 OAuth 流程 │ │ # device-login / poll / login-start / code-relay / callback │ ├── auth/ # 管理员认证 + key 管理策略 │ │ ├── mod.rs @@ -200,7 +200,7 @@ src/ 大部分是目录搬迁,但有 3 个文件需要真正动刀: -1. **`http/admin/accounts.rs`(1697 行)→ `admin/http/accounts/` 9 文件**。按上面 16 个 handler 归类(list/create/mutate/delete/health/quota/cookies/oauth + mod 路由)。 +1. **`http/api/admin/accounts.rs`(1697 行)→ `admin/http/accounts/` 9 文件**。按上面 16 个 handler 归类(list/create/mutate/delete/health/quota/cookies/oauth + mod 路由)。 2. **`service/admin_auth.rs` 拆责**。密码登录/会话校验留 `admin/auth/service.rs`;OAuth token 交换 + 账号导入的编排移到 `admin/http/accounts/oauth.rs`,由它调用 `codex::gateway::oauth`(换 token)+ `codex::accounts::service::import`(落库)。 diff --git a/docs/architecture-reorganization.md b/docs/architecture-reorganization.md index 579374a53..7ef548224 100644 --- a/docs/architecture-reorganization.md +++ b/docs/architecture-reorganization.md @@ -36,7 +36,7 @@ The cleanup started from these pressure points: | `tests/admin_accounts_route_test.rs` | 2588 | Admin account scenarios are too large to scan or move safely. | | `tests/v1_upstream_route_test.rs` | 2115 | Responses HTTP SSE, WebSocket, fallback, error mapping, usage, and logging side effects are mixed. | | `tests/chat_completions_route_test.rs` | 710 | Manageable, but should eventually align with the v1 split. | -| `tests/admin_api_keys_route_test.rs` | 659 | Manageable, but can share common admin test setup. | +| `tests/admin_client_keys_route_test.rs` | 659 | Manageable, but can share common admin test setup. | The main coupling pattern is: @@ -51,8 +51,8 @@ touch fallback, account state, usage logs, and protocol conversion at the same t As of this cleanup pass: -- `src/http/admin/mod.rs` is the admin module entry, backed by resource-specific files - under `src/http/admin/` and a dedicated admin router. +- `src/http/api/admin/mod.rs` is the admin module entry, backed by resource-specific files + under `src/http/api/admin/` and a dedicated admin router. - `src/http/v1/mod.rs` is the v1 module entry, backed by thin route handlers for auth, chat, responses, models, errors, and router mounting under `src/http/v1/`. - `src/service/` now contains backend-facing HTTP use-case services: @@ -78,7 +78,7 @@ As of this cleanup pass: - admin model catalog refresh orchestration lives in `codex::models::service::ModelService`. - `tests/common/` now contains shared integration-test helpers, including generic response/session helpers plus admin account and v1 upstream route fixtures. -- `tests/admin_accounts_route_test.rs` has been split into list, mutation, +- `tests/admin_accounts_route_test.rs` has been split into list, lifecycle, import/export, OAuth/auth, and cookies/quota/refresh/health scenario files. - `tests/v1_upstream_route_test.rs` has been split into Responses HTTP/SSE, Responses WebSocket, upstream fallback/refresh, and upstream error scenario files. @@ -100,8 +100,8 @@ As of this cleanup pass: - `codex/upstream/dispatch.rs`, `fallback.rs`, `refresh.rs`, `stream.rs`, and `usage.rs` now own the obvious helper groups for dispatch response helpers, retry classification, token refresh retry, SSE response collection, and usage recording. -- `codex/accounts/service/import.rs`, `mutation.rs`, `cookies.rs`, `quota.rs`, `health.rs`, - `refresh.rs`, and `runtime_pool.rs` now own account import, mutation, cookie, quota, +- `codex/accounts/service/import.rs`, `lifecycle.rs`, `cookies.rs`, `quota.rs`, `health.rs`, + `refresh.rs`, and `runtime_pool.rs` now own account import, lifecycle, cookie, quota, health-check, single-account refresh, and runtime-pool workflows instead of leaving them in `service/account/mod.rs`. - `ChatService` and `ResponsesService` own a cloned `CodexUpstreamService` instance injected from @@ -128,7 +128,7 @@ As of this cleanup pass: moved out of route tests into `tests/fixtures/*.sse`. - v1 Responses WebSocket mocked response payloads have been moved into `tests/fixtures/*.json`. -- `http/admin/response.rs` now provides `AdminError`, and the admin route modules use +- `http/api/admin/response.rs` now provides `AdminError`, and the admin route modules use `Result<impl IntoResponse, AdminError>` for standard admin-envelope failures. - admin auth handlers now propagate session, login, OAuth, and PKCE failures through `AdminError`; success paths that must set cookies or redirect still build explicit @@ -210,7 +210,7 @@ and the Rust best-practice rules below. ### Async, Sharing, And Trait Objects - Shared runtime state should use `Arc` and explicit synchronization primitives such as - `tokio::sync::Mutex` only where mutation is required. + `tokio::sync::Mutex` only where lifecycle is required. - Keep lock scopes short. Do not hold account-pool or session locks while performing network requests, database calls, or expensive serialization. - Prefer generics/static dispatch for local helper abstractions. Use `Arc<dyn Trait>` @@ -336,7 +336,7 @@ src/ service/ mod.rs import.rs - mutation.rs + lifecycle.rs cookies.rs quota.rs health.rs @@ -505,7 +505,7 @@ This phase should be mostly mechanical: Create: ```text -src/http/admin/ +src/http/api/admin/ mod.rs router.rs response.rs @@ -526,10 +526,10 @@ Suggested ownership: | `auth.rs` | `login`, `auth_status`, `auth_logout`, OAuth PKCE/device routes, admin session cookie helpers. | | `accounts.rs` | account list/create/import/export/delete/status/label, health check, refresh, reset usage, quota, cookies, account import parsing helpers. | | `api_keys.rs` | local client key list/create/import/export/delete/status/label/batch helpers. | -| `logs.rs` | `/admin/logs` handler and log DTOs. | -| `models.rs` | `/admin/refresh-models` handler and model refresh DTOs. | -| `settings.rs` | `/admin/settings` handler and settings DTOs. | -| `usage.rs` | `/admin/usage-stats` and `/admin/usage-stats/summary`. | +| `logs.rs` | `/api/admin/logs` handler and log DTOs. | +| `models.rs` | `/api/admin/refresh-models` handler and model refresh DTOs. | +| `settings.rs` | `/api/admin/settings` handler and settings DTOs. | +| `usage.rs` | `/api/admin/usage-stats` and `/api/admin/usage-stats/summary`. | | `router.rs` | admin route mounting only. | Keep cross-resource helpers private when possible. If a helper is needed by multiple @@ -540,10 +540,10 @@ The admin router should own admin paths: ```rust pub fn router() -> Router<AppState> { Router::new() - .route("/admin/login", post(auth::login)) - .route("/admin/auth/status", get(auth::auth_status)) - .route("/admin/accounts", get(accounts::accounts).post(accounts::create_account)) - .route("/admin/api-keys", get(api_keys::api_keys).post(api_keys::create_api_key)) + .route("/api/admin/login", post(auth::login)) + .route("/api/admin/auth/status", get(auth::auth_status)) + .route("/api/admin/accounts", get(accounts::accounts).post(accounts::create_account)) + .route("/api/admin/api-keys", get(api_keys::api_keys).post(api_keys::create_api_key)) } ``` @@ -600,7 +600,7 @@ cargo test --test admin_accounts_mutation_test cargo test --test admin_accounts_import_export_test cargo test --test admin_accounts_oauth_test cargo test --test admin_accounts_cookies_quota_test -cargo test --test admin_api_keys_route_test +cargo test --test admin_client_keys_route_test cargo test --test v1_responses_http_sse_test cargo test --test v1_responses_websocket_test cargo test --test v1_upstream_fallback_test @@ -758,7 +758,7 @@ Targets: ```text src/codex/protocol/error.rs src/http/response.rs -src/http/admin/response.rs +src/http/api/admin/response.rs src/http/v1/errors.rs ``` @@ -990,7 +990,7 @@ cargo test --test v1_upstream_errors_test ### Step 4: Split Former `src/http/admin.rs` -Move admin code into `src/http/admin/*.rs`, with `src/http/admin/mod.rs` as the module +Move admin code into `src/http/api/admin/*.rs`, with `src/http/api/admin/mod.rs` as the module entry. Preserve behavior. Verification: @@ -1002,7 +1002,7 @@ cargo test --test admin_accounts_mutation_test cargo test --test admin_accounts_import_export_test cargo test --test admin_accounts_oauth_test cargo test --test admin_accounts_cookies_quota_test -cargo test --test admin_api_keys_route_test +cargo test --test admin_client_keys_route_test cargo test --test admin_logs_route_test cargo test --test admin_models_route_test cargo test --test admin_settings_route_test @@ -1090,7 +1090,7 @@ cargo test --test admin_accounts_mutation_test cargo test --test admin_accounts_import_export_test cargo test --test admin_accounts_oauth_test cargo test --test admin_accounts_cookies_quota_test -cargo test --test admin_api_keys_route_test +cargo test --test admin_client_keys_route_test cargo test --test v1_responses_http_sse_test cargo test --test v1_responses_websocket_test cargo test --test v1_upstream_fallback_test diff --git a/docs/backend-naming-audit.md b/docs/backend-naming-audit.md new file mode 100644 index 000000000..d9ba6d3f7 --- /dev/null +++ b/docs/backend-naming-audit.md @@ -0,0 +1,468 @@ +# Backend Naming Audit + +## Verdict + +当前后端命名可以作为第一版完整架构定稿。主边界已经清楚:`runtime` 负责装配,`web` 负责未来 Web 控制台承载,`admin` 负责管理域,`codex` 负责 Codex 业务内核,`platform` 负责跨领域基础设施。 + +命名的艺术不在于词汇华丽,而在于每个名字都能回答三个问题: + +- 这个目录属于哪个领域。 +- 这个文件对外暴露什么责任。 +- 这个名字是否会让后来的人误以为它还承担了别的职责。 + +本轮审计结论:源码目录已经达到“可长期演进”的层级。剩余问题主要是少数兼容性术语和偏大的聚合文件,不影响当前架构定稿。 + +## Naming Principles + +- 路由表面用 `api`、`http`、`web` 表达协议和承载面。 +- 业务能力用领域名表达,例如 `accounts`、`models`、`events`、`gateway`。 +- 基础设施只放在 `platform`,避免业务目录承担系统能力。 +- 测试目录跟随生产目录命名,测试文件表达场景而不是实现细节。 +- 对外兼容词可以保留在 route 和 DTO 中,内部模块优先使用更准确的领域词。 + +## Complete Architecture Tree + +以下目录排除了生成物和本地状态:`.git`、`target`、`data`、`logs`、`designs`。 + +```text +. +├── docs +│ ├── superpowers +│ │ ├── plans +│ │ │ ├── 2026-06-11-codex-proxy-rs.md +│ │ │ ├── 2026-06-12-openai-gpt-codex-parity.md +│ │ │ ├── 2026-06-13-utils-module-migration.md +│ │ │ ├── 2026-06-14-backend-naming-architecture.md +│ │ │ └── 2026-06-14-logging-refactor.md +│ │ └── specs +│ │ ├── 2026-06-12-openai-gpt-codex-parity-design.md +│ │ ├── 2026-06-13-architecture-refactor-design.md +│ │ ├── 2026-06-14-backend-naming-architecture-design.md +│ │ └── 2026-06-14-logging-refactor-design.md +│ ├── ADMIN_INITIALIZATION_FIX.md +│ ├── FINGERPRINT_AUTO_UPDATE.md +│ ├── FINGERPRINT_FIX.md +│ ├── FINGERPRINT_PROPAGATION_FIX.md +│ ├── FINGERPRINT_UPDATE_TEST_REPORT.md +│ ├── IMPLEMENTATION_COMPARISON.md +│ ├── IMPLEMENTATION_COMPLETE.md +│ ├── OPENAI_CODEX_PARITY_AUDIT.md +│ ├── OPENAI_REQUEST_ALIGNMENT.md +│ ├── RELIABILITY_ENHANCEMENTS.md +│ ├── RESPONSE_CHAIN_ALIGNMENT.md +│ ├── api.md +│ ├── architecture-audit.md +│ ├── architecture-capability-plan.md +│ ├── architecture-reorganization.md +│ ├── backend-naming-audit.md +│ ├── code-quality-audit.md +│ ├── database-storage-audit.md +│ ├── dependency-policy.md +│ ├── implementation-status.md +│ ├── optimization-roadmap.md +│ ├── rust-community-review.md +│ ├── scheduler-completion-report.md +│ ├── scheduler-implementation.md +│ ├── scheduler-usage.md +│ └── status-codes.md +├── src +│ ├── admin +│ │ ├── api +│ │ │ ├── accounts +│ │ │ │ ├── cookies.rs +│ │ │ │ ├── create.rs +│ │ │ │ ├── delete.rs +│ │ │ │ ├── export.rs +│ │ │ │ ├── health.rs +│ │ │ │ ├── import.rs +│ │ │ │ ├── lifecycle.rs +│ │ │ │ ├── list.rs +│ │ │ │ ├── mod.rs +│ │ │ │ ├── oauth.rs +│ │ │ │ └── quota.rs +│ │ │ ├── client_keys +│ │ │ │ ├── create.rs +│ │ │ │ ├── export.rs +│ │ │ │ ├── import.rs +│ │ │ │ ├── lifecycle.rs +│ │ │ │ ├── list.rs +│ │ │ │ └── mod.rs +│ │ │ ├── logs +│ │ │ │ ├── detail.rs +│ │ │ │ ├── mod.rs +│ │ │ │ ├── query.rs +│ │ │ │ └── state.rs +│ │ │ ├── diagnostics.rs +│ │ │ ├── mod.rs +│ │ │ ├── models.rs +│ │ │ ├── response.rs +│ │ │ ├── router.rs +│ │ │ ├── session.rs +│ │ │ ├── settings.rs +│ │ │ └── usage.rs +│ │ ├── client_keys +│ │ │ ├── mod.rs +│ │ │ └── service.rs +│ │ ├── session +│ │ │ ├── mod.rs +│ │ │ ├── repository.rs +│ │ │ └── service.rs +│ │ ├── tasks +│ │ │ ├── mod.rs +│ │ │ └── session_cleanup.rs +│ │ ├── mod.rs +│ │ └── settings.rs +│ ├── codex +│ │ ├── accounts +│ │ │ ├── cookies +│ │ │ │ ├── jar.rs +│ │ │ │ ├── mod.rs +│ │ │ │ └── repository.rs +│ │ │ ├── repository +│ │ │ │ ├── accounts.rs +│ │ │ │ ├── leases.rs +│ │ │ │ ├── mod.rs +│ │ │ │ ├── quotas.rs +│ │ │ │ ├── tokens.rs +│ │ │ │ └── usage.rs +│ │ │ ├── service +│ │ │ │ ├── cookies.rs +│ │ │ │ ├── health.rs +│ │ │ │ ├── import.rs +│ │ │ │ ├── lifecycle.rs +│ │ │ │ ├── mod.rs +│ │ │ │ ├── pool_sync.rs +│ │ │ │ ├── quota.rs +│ │ │ │ └── refresh.rs +│ │ │ ├── cloudflare_challenge.rs +│ │ │ ├── jwt.rs +│ │ │ ├── lifecycle.rs +│ │ │ ├── mod.rs +│ │ │ ├── model.rs +│ │ │ ├── pool.rs +│ │ │ └── usage_snapshots.rs +│ │ ├── events +│ │ │ ├── event.rs +│ │ │ ├── mod.rs +│ │ │ ├── repository.rs +│ │ │ └── service.rs +│ │ ├── gateway +│ │ │ ├── fingerprint +│ │ │ │ ├── mod.rs +│ │ │ │ ├── model.rs +│ │ │ │ ├── repository.rs +│ │ │ │ ├── update_checker.rs +│ │ │ │ └── updater.rs +│ │ │ ├── oauth +│ │ │ │ ├── client.rs +│ │ │ │ ├── codex_cli.rs +│ │ │ │ ├── mod.rs +│ │ │ │ ├── refresh.rs +│ │ │ │ └── token.rs +│ │ │ ├── protocol +│ │ │ │ ├── codex_to_openai.rs +│ │ │ │ ├── error.rs +│ │ │ │ ├── mod.rs +│ │ │ │ ├── openai_to_codex.rs +│ │ │ │ └── schema.rs +│ │ │ ├── transport +│ │ │ │ ├── websocket +│ │ │ │ │ ├── codec.rs +│ │ │ │ │ ├── mod.rs +│ │ │ │ │ └── pool.rs +│ │ │ │ ├── headers.rs +│ │ │ │ ├── http_client.rs +│ │ │ │ ├── mod.rs +│ │ │ │ ├── rate_limits.rs +│ │ │ │ ├── sse.rs +│ │ │ │ ├── types.rs +│ │ │ │ └── usage_events.rs +│ │ │ ├── conversation_identity.rs +│ │ │ ├── installation_id.rs +│ │ │ └── mod.rs +│ │ ├── models +│ │ │ ├── catalog.rs +│ │ │ ├── mod.rs +│ │ │ ├── repository.rs +│ │ │ └── service.rs +│ │ ├── serving +│ │ │ ├── dispatch +│ │ │ │ ├── account_refresh.rs +│ │ │ │ ├── affinity.rs +│ │ │ │ ├── fallback.rs +│ │ │ │ ├── limits.rs +│ │ │ │ ├── mod.rs +│ │ │ │ ├── routing.rs +│ │ │ │ ├── stream.rs +│ │ │ │ ├── stream_audit.rs +│ │ │ │ └── usage.rs +│ │ │ ├── http +│ │ │ │ ├── auth.rs +│ │ │ │ ├── chat.rs +│ │ │ │ ├── diagnostics.rs +│ │ │ │ ├── errors.rs +│ │ │ │ ├── mod.rs +│ │ │ │ ├── models.rs +│ │ │ │ ├── responses.rs +│ │ │ │ └── router.rs +│ │ │ ├── chat.rs +│ │ │ ├── diagnostics.rs +│ │ │ ├── mod.rs +│ │ │ └── responses.rs +│ │ ├── tasks +│ │ │ ├── mod.rs +│ │ │ ├── model_refresh.rs +│ │ │ ├── quota_refresh.rs +│ │ │ └── token_refresh.rs +│ │ ├── usage +│ │ │ ├── mod.rs +│ │ │ └── service.rs +│ │ └── mod.rs +│ ├── config +│ │ ├── loader.rs +│ │ ├── mod.rs +│ │ └── types.rs +│ ├── platform +│ │ ├── crypto +│ │ │ ├── mod.rs +│ │ │ └── secret_box.rs +│ │ ├── http +│ │ │ ├── auth.rs +│ │ │ ├── health.rs +│ │ │ ├── mod.rs +│ │ │ └── request_id.rs +│ │ ├── identity +│ │ │ ├── admin_session.rs +│ │ │ ├── client_key.rs +│ │ │ ├── client_key_repository.rs +│ │ │ ├── error.rs +│ │ │ └── mod.rs +│ │ ├── logging +│ │ │ ├── mod.rs +│ │ │ └── rotation.rs +│ │ ├── storage +│ │ │ ├── db.rs +│ │ │ ├── mod.rs +│ │ │ ├── paths.rs +│ │ │ └── schema.sql +│ │ └── mod.rs +│ ├── runtime +│ │ ├── tasks +│ │ │ ├── coordinator.rs +│ │ │ ├── mod.rs +│ │ │ └── types.rs +│ │ ├── bootstrap.rs +│ │ ├── mod.rs +│ │ ├── router.rs +│ │ └── state.rs +│ ├── utils +│ │ ├── json.rs +│ │ ├── mod.rs +│ │ └── pagination.rs +│ ├── web +│ │ ├── assets.rs +│ │ ├── mod.rs +│ │ ├── router.rs +│ │ ├── security.rs +│ │ └── shell.rs +│ ├── lib.rs +│ └── main.rs +├── tests +│ ├── admin +│ │ ├── accounts +│ │ │ ├── cookies_quota.rs +│ │ │ ├── import_export.rs +│ │ │ ├── lifecycle.rs +│ │ │ ├── list.rs +│ │ │ └── oauth.rs +│ │ ├── api_contract.rs +│ │ ├── client_keys_route.rs +│ │ ├── logs_route.rs +│ │ ├── models_route.rs +│ │ ├── session.rs +│ │ ├── session_login_route.rs +│ │ ├── session_repository.rs +│ │ ├── settings_route.rs +│ │ └── usage_stats_route.rs +│ ├── architecture +│ │ ├── accounts_boundary.rs +│ │ ├── admin_accounts_split.rs +│ │ ├── admin_boundary.rs +│ │ ├── codex_activity_boundary.rs +│ │ ├── gateway_boundary.rs +│ │ ├── platform_boundary.rs +│ │ ├── runtime_tasks_boundary.rs +│ │ └── serving_boundary.rs +│ ├── codex_accounts +│ │ ├── cookie_store.rs +│ │ ├── pool_scheduling.rs +│ │ ├── refresh.rs +│ │ ├── refresh_scheduler.rs +│ │ ├── repository.rs +│ │ └── service_refresh.rs +│ ├── codex_events +│ │ └── logs_pagination.rs +│ ├── codex_gateway +│ │ ├── websocket +│ │ │ └── pool.rs +│ │ ├── cli_auth_import.rs +│ │ ├── fingerprint_update.rs +│ │ ├── headers.rs +│ │ ├── http_client.rs +│ │ ├── oauth_refresh.rs +│ │ ├── usage_events.rs +│ │ └── websocket.rs +│ ├── codex_models +│ │ └── catalog.rs +│ ├── codex_serving +│ │ ├── chat_completions.rs +│ │ ├── diagnostics_route.rs +│ │ ├── responses_http_sse.rs +│ │ ├── responses_websocket.rs +│ │ ├── routes_chat.rs +│ │ ├── routes_responses.rs +│ │ ├── upstream_errors.rs +│ │ └── upstream_fallback.rs +│ ├── fixtures +│ │ ├── chat +│ │ │ ├── non_stream_text.sse +│ │ │ ├── parallel_tools_success.sse +│ │ │ ├── retry_after_success.sse +│ │ │ ├── stream_text.sse +│ │ │ ├── tool_reasoning_complete.sse +│ │ │ └── tool_stream.sse +│ │ └── responses +│ │ ├── http_sse +│ │ │ ├── after_402.sse +│ │ │ ├── after_403.sse +│ │ │ ├── after_429.sse +│ │ │ ├── after_cloudflare_403.sse +│ │ │ ├── after_refresh.sse +│ │ │ ├── completed_fields.sse +│ │ │ ├── completed_reasoning_include.sse +│ │ │ ├── completed_usage.sse +│ │ │ ├── default_stream.sse +│ │ │ ├── done_item_completed.sse +│ │ │ ├── error_event.sse +│ │ │ ├── failed_event.sse +│ │ │ ├── stream_after_429.sse +│ │ │ ├── stream_after_refresh.sse +│ │ │ ├── stream_error_event.sse +│ │ │ ├── stream_failed_event.sse +│ │ │ ├── stream_usage.sse +│ │ │ └── text_deltas_completed.sse +│ │ └── websocket +│ │ ├── completed.json +│ │ ├── first_account_limited.json +│ │ ├── history_rate_limited.json +│ │ ├── rate_limited.json +│ │ ├── second_account_limited.json +│ │ └── token_revoked.json +│ ├── platform +│ │ ├── client_key_auth.rs +│ │ ├── crypto.rs +│ │ ├── http_auth.rs +│ │ ├── log_rotation.rs +│ │ └── storage_schema.rs +│ ├── runtime +│ │ ├── http_trace.rs +│ │ └── startup.rs +│ ├── support +│ │ ├── admin_accounts.rs +│ │ ├── mod.rs +│ │ └── upstream.rs +│ ├── admin.rs +│ ├── architecture.rs +│ ├── codex_accounts.rs +│ ├── codex_events.rs +│ ├── codex_gateway.rs +│ ├── codex_models.rs +│ ├── codex_serving.rs +│ ├── config.rs +│ ├── platform.rs +│ └── runtime.rs +├── .gitignore +├── Cargo.lock +├── Cargo.toml +├── Dockerfile +├── README.md +├── config.yaml +├── docker-compose.yml +└── rust-toolchain.toml +``` + +## Domain Audit + +| Area | Verdict | Notes | +| --- | --- | --- | +| `src/runtime` | Good | `bootstrap`、`router`、`state`、`tasks` 表达装配层职责,适合单 crate 起步阶段。 | +| `src/web` | Good | 只作为未来 Admin Web 控制台的后端挂载面,没有前端历史包袱。 | +| `src/admin/api` | Good | 管理 JSON API 从 `http` 收束为 `api`,比协议名更能表达契约层。 | +| `src/admin/session` | Good | 管理员登录态与 `client_keys` 拆开,避免 `auth` 泛化。 | +| `src/admin/client_keys` | Good | 内部语义是本地客户端凭据,不是上游 provider key。 | +| `src/codex/accounts` | Good | 账号、cookies、仓储、服务、调度池都在账号域内。 | +| `src/codex/models` | Good | model catalog 从账号域剥离,边界正确。 | +| `src/codex/events` | Good | 业务事件从进程日志中独立出来,命名比 `logs` 更准确。 | +| `src/codex/gateway` | Good | `protocol`、`transport`、`oauth`、`fingerprint` 是上游链路的自然分层。 | +| `src/codex/serving` | Good | 对外 OpenAI-compatible 服务面与上游 gateway 分离。 | +| `src/platform` | Good | `crypto`、`http`、`identity`、`logging`、`storage` 都是跨领域能力。 | +| `src/config` | Good | 配置加载与类型独立,暂不需要进一步艺术化命名。 | +| `src/utils` | Acceptable | 当前只有 `json` 和 `pagination`,还没变成杂物间;后续如果扩张,应优先迁回领域目录。 | + +## File Naming Audit + +### Names To Keep + +- `admin/api/accounts/lifecycle.rs`:覆盖状态、标签、删除、批量变更,比 `mutate.rs` 更准确。 +- `admin/api/client_keys/*`:内部使用 `client_keys` 是正确选择,路由保留 `/api-keys` 是对外兼容。 +- `codex/accounts/cloudflare_challenge.rs`:比 `cf_path_block.rs` 明确,表达真实安全链路问题。 +- `codex/accounts/service/pool_sync.rs`:表达账号仓储与运行时池同步,比 `runtime_pool.rs` 更有边界。 +- `codex/gateway/conversation_identity.rs`:比泛化 `identity.rs` 更准确。 +- `codex/gateway/installation_id.rs`:表达 ChatGPT/Codex 安装标识,不混入一般 installation 概念。 +- `codex/gateway/transport/http_client.rs`:比 `client.rs` 清楚,因为 transport 下还有 WebSocket。 +- `codex/gateway/transport/usage_events.rs`:表达从上游事件流中提取 usage,而不是通用 usage 领域。 +- `codex/tasks/{model_refresh,quota_refresh,token_refresh}.rs`:任务以动作命名,职责清晰。 +- `platform/http/request_id.rs`:比 `middleware.rs` 精确。 +- `platform/crypto/secret_box.rs`:表达加密封装能力,适合稳定复用。 + +### Names That Are Acceptable But Can Be Refined Later + +- `src/codex/accounts/model.rs`:目前承载 `Account` 和 `AccountStatus`,与 `codex/models` 有轻微视觉冲突。若继续追求极致,可改成 `account.rs`,让 `accounts/account.rs` 与 `models/catalog.rs` 完全错开。 +- `src/admin/api/client_keys/*` 内部 handler 名仍使用 `api_key`,例如 `create_api_key`。这是对外路由 `/api/admin/api-keys` 的兼容词,不是架构错误。若后续精修,可把 handler 改为 `create_client_key`,DTO 保留 `ClientApiKey*`。 +- `tests/codex_events/logs_pagination.rs`:目录已经叫 `codex_events`,文件还叫 `logs_pagination`。如果按领域纯度,可改为 `event_log_pagination.rs`。 +- `tests/admin/*_route.rs`:测试文件显式带 `route`,表达 HTTP 路由测试,可以保留。若未来测试分层更细,可迁到 `tests/admin/api/*`。 +- `src/codex/gateway/transport/types.rs`:`types` 是弱名,但当前范围在 `transport` 下仍可接受;如果继续增长,应按 request/response/error 拆出更具体文件。 + +### Names To Avoid Reintroducing + +- `admin/http`:会把管理 API 误导为协议工具层。 +- `admin/auth`:同时包含 session 和 client key 时语义过宽。 +- `codex/accounts/models`:会让 model catalog 看起来像账号私有数据。 +- `codex/logs`:会混淆业务事件和进程日志。 +- `platform/http/middleware.rs`:middleware 是技术位置,不是责任名。 +- `gateway/identity.rs`:identity 太泛,会掩盖 conversation identity 的具体含义。 +- `transport/client.rs`:在有 HTTP、SSE、WebSocket 多传输时过于笼统。 + +## Size And Organization Audit + +命名已经过关,但少数文件仍偏大,后续可在不改变逻辑的前提下继续整理: + +- `src/codex/serving/dispatch/mod.rs`:约 1000 行,是下一轮最值得拆的聚合点。建议按 request context、response assembly、fallback decision、event recording 继续切分。 +- `src/codex/gateway/transport/http_client.rs`:约 780 行。可拆为 request building、response parsing、model fetching、probe 四块。 +- `src/codex/accounts/repository/accounts.rs`:约 700 行。可按 load/list/write/status 或 token metadata 继续切分。 +- `src/codex/accounts/pool.rs`、`src/codex/serving/dispatch/affinity.rs`、`src/codex/gateway/transport/websocket/*` 当前偏大但仍有领域凝聚力,暂不强拆。 + +这些不是命名阻断项。继续优化时应遵守“只拆大文件,不改逻辑”,每次拆分后跑 `cargo fmt --check`、`cargo test --locked`、`cargo clippy --all-targets --all-features --locked -- -D warnings`。 + +## Final Recommendation + +当前架构可以定为: + +```text +runtime -> web/admin/codex/platform/config/utils +admin -> api/session/client_keys/tasks/settings +codex -> accounts/models/gateway/serving/events/usage/tasks +platform-> crypto/http/identity/logging/storage +``` + +这是一个适合项目起步阶段的完整体:没有过度抽象,没有强行 workspace 化,也没有把 Web 前端塞进后端。后续前端可以独立演进,后端只保留 `src/web` 作为运行时挂载点。 diff --git a/docs/database-storage-audit.md b/docs/database-storage-audit.md index 4a1cc02fb..66c1d1aaa 100644 --- a/docs/database-storage-audit.md +++ b/docs/database-storage-audit.md @@ -64,7 +64,7 @@ Claude 审计中“窗口字段未持久化”的结论成立,但“配额控 - v1 response 和 401 后账户刷新失败事件通过 `LogService::record` 写入。 - 在一个入口执行 `enabled`、`capacity`、`capture_body` 和 metadata 规范。 -- 管理端 `/admin/logs/state` 和请求链路共享同一个 `LogService` 实例,运行时开关立即生效。 +- 管理端 `/api/admin/logs/state` 和请求链路共享同一个 `LogService` 实例,运行时开关立即生效。 ### P2:收敛 schema 约束(已落地) diff --git a/docs/implementation-status.md b/docs/implementation-status.md index ab5801bb0..e18c8fa62 100644 --- a/docs/implementation-status.md +++ b/docs/implementation-status.md @@ -5,23 +5,23 @@ Update this file before each feature commit. | Area | Status | Commit | Verification | Notes | | --- | --- | --- | --- | --- | | Scaffold | Completed | `fc47967` | `cargo fmt --check`; `cargo test`; `cargo clippy --all-targets --all-features --locked -- -D warnings` | Rust crate, lint policy, pinned TLS-sensitive dependencies, `.gitignore`, and minimal entry points are in place. | -| API contract docs | Completed | `fc47967` | `cargo fmt --check`; `cargo test`; `cargo clippy --all-targets --all-features --locked -- -D warnings` | `/v1/*` OpenAI-compatible body, `/admin/*` frontend envelope, body codes, camelCase, and request ID policy are documented. | +| API contract docs | Completed | `fc47967` | `cargo fmt --check`; `cargo test`; `cargo clippy --all-targets --all-features --locked -- -D warnings` | `/v1/*` OpenAI-compatible body, `/api/admin/*` frontend envelope, body codes, camelCase, and request ID policy are documented. | | Configuration | Completed | `9ed8579` + `e0680bf` + `cfec8ba` | `cargo test default_config_keeps_only_codex_backend`; `cargo test config_loader_merges_config_and_local_yaml`; full `fmt/test/clippy` before commit | Defines Codex-only config, root `config.yaml` plus optional local YAML loading, `Arc<AppServices>` state shell, `gpt-5.5` default model, and in-scope model/auth/quota/usage/logging settings fields without proxy/Ollama/provider/OpenAI-key knobs. | -| Admin settings | Completed | `7424a47` | `cargo test --test admin_settings_route_test admin_settings_patch`; `cargo test --test admin_settings_route_test` | Adds admin-session-gated `PATCH /admin/settings` for retained model/auth/quota/logging/usage settings. Mutations validate retained fields, reject removed proxy/Ollama/provider/OpenAI-key/Electron settings with admin error envelopes, write a root `local.yaml` overlay, and update the same-process settings view. Already constructed runtime services still use their construction-time config until a later runtime reconfiguration task or service restart. | +| Admin settings | Completed | `7424a47` | `cargo test --test admin_settings_route_test admin_settings_patch`; `cargo test --test admin_settings_route_test` | Adds admin-session-gated `PATCH /api/admin/settings` for retained model/auth/quota/logging/usage settings. Mutations validate retained fields, reject removed proxy/Ollama/provider/OpenAI-key/Electron settings with admin error envelopes, write a root `local.yaml` overlay, and update the same-process settings view. Already constructed runtime services still use their construction-time config until a later runtime reconfiguration task or service restart. | | SQLite storage | Completed | `ffec8b5` + `b0c10f2` | `cargo test sqlite_schema_creates_accounts_and_event_tables`; `cargo test connect_sqlite_creates_parent_directory_for_file_database`; full `fmt/test/clippy` before commit | Creates SQLite WAL connector, parent directory bootstrap, sqlx-managed schema for accounts/API keys/sessions/cookies/fingerprints/events, and event/account indexes. | -| Admin auth and client API keys | Partial | `24d5799` + `4e0d1e5` + `e9fdea0` + `8af350d` + `960bbb2` | `cargo test client_api_key_has_proxy_prefix_and_verifies_against_hash`; `cargo test admin_password_hash_is_not_a_client_api_key`; `cargo test --test admin_login_route_test`; `cargo test --test admin_api_keys_route_test`; `cargo fmt --check`; `cargo test`; `cargo clippy --all-targets --all-features --locked -- -D warnings`; full `fmt/test/clippy` before `8af350d`; full `fmt/test/clippy` before `960bbb2`; current local key import/export targeted `cargo test --test admin_api_keys_route_test` | Admin passwords use Argon2id, client API keys use `cpr_` prefix plus HMAC-SHA256 with server-side pepper; `/admin/login` issues HttpOnly sessions and rejects client API keys as admin credentials. `/admin/api-keys` can create/list local client keys, status-enable/status-disable local client keys, label local client keys, delete local client keys, batch-delete local client keys, export local client key metadata, import metadata by rotating to new local `cpr_` keys, and `/v1/*` verifies only enabled keys against SQLite instead of accepting any `cpr_` string. These routes are Rust-local client auth utilities, not TypeScript third-party provider API-key parity. Provider API key pools and provider key import/export remain out of scope; local `cpr_` key plaintext and key hashes are not exportable after HMAC storage, and import returns newly generated plaintext values only once. | +| Admin auth and client API keys | Partial | `24d5799` + `4e0d1e5` + `e9fdea0` + `8af350d` + `960bbb2` | `cargo test client_api_key_has_proxy_prefix_and_verifies_against_hash`; `cargo test admin_password_hash_is_not_a_client_api_key`; `cargo test --test admin_login_route_test`; `cargo test --test admin_client_keys_route_test`; `cargo fmt --check`; `cargo test`; `cargo clippy --all-targets --all-features --locked -- -D warnings`; full `fmt/test/clippy` before `8af350d`; full `fmt/test/clippy` before `960bbb2`; current local key import/export targeted `cargo test --test admin_client_keys_route_test` | Admin passwords use Argon2id, client API keys use `cpr_` prefix plus HMAC-SHA256 with server-side pepper; `/api/admin/login` issues HttpOnly sessions and rejects client API keys as admin credentials. `/api/admin/api-keys` can create/list local client keys, status-enable/status-disable local client keys, label local client keys, delete local client keys, batch-delete local client keys, export local client key metadata, import metadata by rotating to new local `cpr_` keys, and `/v1/*` verifies only enabled keys against SQLite instead of accepting any `cpr_` string. These routes are Rust-local client auth utilities, not TypeScript third-party provider API-key parity. Provider API key pools and provider key import/export remain out of scope; local `cpr_` key plaintext and key hashes are not exportable after HMAC storage, and import returns newly generated plaintext values only once. | | Secret encryption | Completed | `105aa58` + `228febd` | `cargo test secret_box_encrypts_and_decrypts_without_storing_plaintext`; `cargo test secret_box_load_or_create_should_reuse_existing_master_key_file`; full `fmt/test/clippy` before commit | AES-256-GCM `SecretBox` stores upstream secrets as `v1:<nonce>:<ciphertext>` without plaintext and can load/create the runtime master key file used by repositories. | -| Logging and pagination | Completed | `eb0341e` + `fc16c48` + `b0c10f2` + `ee0ed90` + `fdb9374` + `5bf012c` + `8645b93` + `35f9b80` + `bae084b` | `cargo test event_logs_are_cursor_paginated`; `cargo test admin_logs_are_cursor_paginated_and_include_request_id`; `cargo test --test admin_logs_route_test admin_logs_state_patch`; `cargo test --test admin_logs_route_test admin_logs_should_filter`; `cargo test --test admin_logs_route_test admin_logs_`; `cargo test rotating_writer_splits_files_when_size_limit_is_reached`; `cargo test --test admin_usage_stats_route_test`; `cargo test --test v1_upstream_route_test`; full `fmt/test/clippy` before commit | Adds cursor pagination, `Page<T>` camelCase serialization, admin `/admin/logs` query/detail/state/update/clear APIs, admin `/admin/logs` filters for kind/level/requestId/accountId/route/model/statusCode/search, admin `/admin/usage-stats` and `/admin/usage-stats/summary`, event log repository, runtime tracing initialization, HTTP trace layer, date/size-based file rotation, v1 response lifecycle events with request/account/route/model/status/latency/non-secret metadata, and non-secret `account.refresh` events for upstream-401 refresh failures. Runtime log state updates are in-process only; persistent config changes remain under `/admin/settings`. | -| Diagnostics | Completed | `4a91dea` + `924e6f8` + `bb143e7` + `689a514` | `cargo test --test diagnostics_route_test admin_diagnostics`; `cargo test --test diagnostics_route_test debug_upstream`; `cargo test --test diagnostics_route_test` | Adds admin-session-gated `GET /admin/diagnostics` plus local-only `GET /debug/diagnostics`, `GET /debug/fingerprint`, and `GET /debug/upstream` with runtime package metadata, config/database/log/security paths, Codex backend transport settings, static Desktop fingerprint summary and standalone fingerprint debug output, account repository/pool state, runtime capacity summary, retained settings summary, and a Codex models endpoint connectivity probe without tokens or upstream bodies. Remote forwarded debug requests are rejected with `403`. | +| Logging and pagination | Completed | `eb0341e` + `fc16c48` + `b0c10f2` + `ee0ed90` + `fdb9374` + `5bf012c` + `8645b93` + `35f9b80` + `bae084b` | `cargo test event_logs_are_cursor_paginated`; `cargo test admin_logs_are_cursor_paginated_and_include_request_id`; `cargo test --test admin_logs_route_test admin_logs_state_patch`; `cargo test --test admin_logs_route_test admin_logs_should_filter`; `cargo test --test admin_logs_route_test admin_logs_`; `cargo test rotating_writer_splits_files_when_size_limit_is_reached`; `cargo test --test admin_usage_stats_route_test`; `cargo test --test v1_upstream_route_test`; full `fmt/test/clippy` before commit | Adds cursor pagination, `Page<T>` camelCase serialization, admin `/api/admin/logs` query/detail/state/update/clear APIs, admin `/api/admin/logs` filters for kind/level/requestId/accountId/route/model/statusCode/search, admin `/api/admin/usage-stats` and `/api/admin/usage-stats/summary`, event log repository, runtime tracing initialization, HTTP trace layer, date/size-based file rotation, v1 response lifecycle events with request/account/route/model/status/latency/non-secret metadata, and non-secret `account.refresh` events for upstream-401 refresh failures. Runtime log state updates are in-process only; persistent config changes remain under `/api/admin/settings`. | +| Diagnostics | Completed | `4a91dea` + `924e6f8` + `bb143e7` + `689a514` | `cargo test --test diagnostics_route_test admin_diagnostics`; `cargo test --test diagnostics_route_test debug_upstream`; `cargo test --test diagnostics_route_test` | Adds admin-session-gated `GET /api/admin/diagnostics` plus local-only `GET /debug/diagnostics`, `GET /debug/fingerprint`, and `GET /debug/upstream` with runtime package metadata, config/database/log/security paths, Codex backend transport settings, static Desktop fingerprint summary and standalone fingerprint debug output, account repository/pool state, runtime capacity summary, retained settings summary, and a Codex models endpoint connectivity probe without tokens or upstream bodies. Remote forwarded debug requests are rejected with `403`. | | TLS headers and fingerprint | Completed | `1bdd08c` | `cargo test codex_headers_include_desktop_identity_and_turn_state`; `cargo tree \| rg 'reqwest\|rustls'`; full `fmt/test/clippy` before commit | Adds Codex Desktop fingerprint model, exact identity/request headers, and pinned reqwest/rustls client builder. | | Cookie persistence | Completed | `4710926` + `5d3cc6e` + `203270f` | `cargo test cookie_jar_captures_and_replays_account_scoped_cookies`; `cargo test cookie_repository_encrypts_and_replays_account_scoped_cookies`; full `fmt/test/clippy` before `203270f` | Adds account-scoped in-memory Cookie jar and encrypted SQLite Cookie repository; Cookies are replayed only for the same account/domain. Admin-pasted browser Cookie headers are stored encrypted and can be cleared per account. | -| Account pool and refresh | Partial | `0f97c5c` + `2f25db8` + `4dc0c9a` + `c2cf7a6` + `636fa79` + `fdb9374` + `3ecd5b6` + `3960a35` + `db6cb98` + `93cfee3` + `6d18d2c` + `a32a53c` + `1d6c301` + `a3b4d2b` + `0daa96b` | `cargo test account_pool_skips_expired_disabled_banned_and_quota_exhausted_accounts`; `cargo test --test account_pool_scheduling_test`; `cargo test --test account_refresh_test`; `cargo test --test oauth_refresh_test`; `cargo test --test v1_upstream_route_test`; `cargo test --test account_pool_startup_test`; `cargo test --test account_pool_startup_test app_state_should_restore_persisted_cooldowns_into_runtime_pool`; `cargo test --test codex_websocket_test`; Task 5 targeted `cargo test --test v1_upstream_route_test`, `cargo test --test chat_completions_route_test`, `cargo test --test account_pool_scheduling_test`, and WebSocket fallback route tests; Task 7 targeted `cargo test --test admin_accounts_route_test`; auth status/logout targeted `cargo test --test admin_accounts_route_test`; device login targeted `cargo test --test admin_accounts_route_test admin_auth_device`, `cargo test --test oauth_refresh_test`, and `cargo test --test admin_accounts_route_test`; PKCE auth-flow targeted `cargo test --test admin_accounts_route_test admin_auth_`, `cargo test --test oauth_refresh_test`, and `cargo test --test admin_accounts_route_test`; full `fmt/test/clippy` before `db6cb98` | Implements base account status filtering, refresh scheduler semantics, real OpenAI OAuth refresh-token exchange, OpenAI OAuth device-code polling into the shared validated account import path, OpenAI OAuth PKCE authorization-code exchange into the same path, 401-triggered refresh and single retry, max concurrent slots, stale slot cleanup, least-used/round-robin/sticky strategies, tier priority, quota-cache filtering, Cloudflare cooldown skip, request staggering metadata, preferred account selection, exclude IDs, capacity summary, settings-backed pool construction, same-process manual/import/CLI/device/PKCE import-to-pool sync, startup pool snapshots from SQLite with usage-based last-used metadata and persisted quota/Cloudflare cooldown state, SQLite-backed 429 quota cooldown marking, Cloudflare cooldown mutation, pool status mutation for upstream fallback, admin label/status/delete/runtime refresh synchronization, admin reset-usage last-used clearing, runtime pool clearing on logout, shared fallback acquisition for non-streaming Responses, HTTP SSE Responses setup, Chat Completions, and non-history WebSocket handshakes before request body send. `previous_response_id` WebSocket requests remain account-affine on classified failures. Still missing durable release-time slot state. | +| Account pool and refresh | Partial | `0f97c5c` + `2f25db8` + `4dc0c9a` + `c2cf7a6` + `636fa79` + `fdb9374` + `3ecd5b6` + `3960a35` + `db6cb98` + `93cfee3` + `6d18d2c` + `a32a53c` + `1d6c301` + `a3b4d2b` + `0daa96b` | `cargo test account_pool_skips_expired_disabled_banned_and_quota_exhausted_accounts`; `cargo test --test account_pool_scheduling_test`; `cargo test --test account_refresh_test`; `cargo test --test oauth_refresh_test`; `cargo test --test v1_upstream_route_test`; `cargo test --test account_pool_startup_test`; `cargo test --test account_pool_startup_test app_state_should_restore_persisted_cooldowns_into_runtime_pool`; `cargo test --test codex_websocket_test`; Task 5 targeted `cargo test --test v1_upstream_route_test`, `cargo test --test chat_completions_route_test`, `cargo test --test account_pool_scheduling_test`, and WebSocket fallback route tests; Task 7 targeted `cargo test --test admin_accounts_route_test`; auth status/logout targeted `cargo test --test admin_accounts_route_test`; device login targeted `cargo test --test admin_accounts_route_test admin_auth_device`, `cargo test --test oauth_refresh_test`, and `cargo test --test admin_accounts_route_test`; PKCE auth-flow targeted `cargo test --test admin_accounts_route_test admin_auth_`, `cargo test --test oauth_refresh_test`, and `cargo test --test admin_accounts_route_test`; full `fmt/test/clippy` before `db6cb98` | Implements base account status filtering, refresh scheduler semantics, real OpenAI OAuth refresh-token exchange, OpenAI OAuth device-code polling into the shared validated account import path, OpenAI OAuth PKCE authorization-code exchange into the same path, 401-triggered refresh and single retry, max concurrent slots, stale slot cleanup, least-used/round-robin/sticky strategies, tier priority, quota-cache filtering, Cloudflare cooldown skip, request staggering metadata, preferred account selection, exclude IDs, capacity summary, settings-backed pool construction, same-process manual/import/CLI/device/PKCE import-to-pool sync, startup pool snapshots from SQLite with usage-based last-used metadata and persisted quota/Cloudflare cooldown state, SQLite-backed 429 quota cooldown marking, Cloudflare cooldown lifecycle, pool status lifecycle for upstream fallback, admin label/status/delete/runtime refresh synchronization, admin reset-usage last-used clearing, runtime pool clearing on logout, shared fallback acquisition for non-streaming Responses, HTTP SSE Responses setup, Chat Completions, and non-history WebSocket handshakes before request body send. `previous_response_id` WebSocket requests remain account-affine on classified failures. Still missing durable release-time slot state. | | Account repository | Completed | `14fe706` + `db6cb98` + `72438c6` + `869b6f8` + `93cfee3` + `a32a53c` + `b879dd6` + `0daa96b` | `cargo test --test account_repository_test`; `cargo test --test admin_accounts_route_test`; `cargo test --test admin_accounts_cookies_quota_test`; `cargo test --test account_pool_startup_test app_state_should_restore_persisted_cooldowns_into_runtime_pool`; `cargo test --test v1_upstream_fallback_test v1_responses_should_`; `cargo fmt --check`; `cargo clippy --all-targets --all-features --locked -- -D warnings`; `cargo test`; full `fmt/test/clippy` before `db6cb98`; full `fmt/test/clippy` before `72438c6`; auth status/logout targeted `cargo test --test admin_accounts_route_test`; quota warnings targeted and full gate before `b879dd6` | Adds encrypted SQLite account token storage, decrypting reads, metadata-only reads for auth status, cursor pagination, full export reads, status/label updates, account deletion, account-wide delete for logout, release-time usage counter persistence, refresh-token-preserving token updates, ChatGPT account/user identity lookup, JWT-claim metadata/token updates that preserve existing labels, quota JSON caching, quota warning snapshot reads, persisted quota/Cloudflare cooldown fields for runtime pool restore, and per-account local usage counter reset. | | Translation | Completed | `eff182e` + `b9c30ba` | `cargo test --test routes_chat_test`; `cargo test --test chat_completions_route_test`; full `cargo fmt --check && cargo test && cargo clippy --all-targets --all-features --locked -- -D warnings` before `b9c30ba` | Adds OpenAI Chat request types, expanded Codex Responses request type, OpenAI Chat to Codex translation for system/developer instructions, multimodal content, tool/function calls, response formats, reasoning effort, service tier, and parallel tool calls. Codex SSE now converts back to OpenAI chat completions and chat chunks with text deltas, reasoning content, tool calls, usage details, and OpenAI-compatible error bodies. | -| HTTP routes | Partial | `ab0ad8f` + `3830b21` + `6179d44` + `cfec8ba` + `6341336` + `228febd` + `c2cf7a6` + `e9fdea0` + `ee0ed90` + `fdb9374` + `9ca5c7d` + `b9c30ba` + `350ffbe` + `3ecd5b6` + `3960a35` + `1738f24` + `0b6cc7e` + `f75db60` + `4b19846` + `db6cb98` + `16eb4ed` + `8af350d` + `203270f` + `960bbb2` + `72438c6` + `16938e6` + `869b6f8` + `e1f35e0` + `93cfee3` + `bfe561d` + `cf5ff0b` + `6d18d2c` + `a32a53c` + `1d6c301` + `a3b4d2b` + `7424a47` + `8645b93` + `35f9b80` + `4a91dea` + `924e6f8` + `bb143e7` + `689a514` + `b879dd6` + `0daa96b` + `efbbe8a` + `bae084b` | `cargo test v1_requires_client_api_key_not_admin_cookie`; `cargo test models_route_returns_openai_compatible_codex_model_list`; `cargo test --test admin_settings_route_test`; `cargo test --test model_catalog_test`; `cargo test --test admin_accounts_route_test`; `cargo test --test cli_auth_import_test`; `cargo test --test admin_api_keys_route_test`; `cargo test --test admin_usage_stats_route_test`; `cargo test --test admin_accounts_cookies_quota_test`; `cargo test --test v1_upstream_route_test`; `cargo test --test chat_completions_route_test`; `cargo test --test codex_websocket_test`; targeted HTTP SSE error tests before `bfe561d`; targeted local API key import/export tests before `cf5ff0b`; WebSocket fallback targeted route tests; auth status/logout targeted `cargo test --test admin_accounts_route_test`; device login targeted `cargo test --test admin_accounts_route_test admin_auth_device`, `cargo test --test oauth_refresh_test`, and `cargo test --test admin_accounts_route_test`; PKCE auth-flow targeted `cargo test --test admin_accounts_route_test admin_auth_`, `cargo test --test oauth_refresh_test`, and `cargo test --test admin_accounts_route_test`; full `cargo fmt --check && cargo test && cargo clippy --all-targets --all-features --locked -- -D warnings` before `b9c30ba` and `350ffbe`; Task 5 targeted verification before `3ecd5b6` and `3960a35`; full `fmt/test/clippy` before `1738f24`; full `fmt/test/clippy` plus real-file smoke before `0b6cc7e`; full `fmt/test/clippy` before `f75db60`; full `fmt/test/clippy` before `4b19846`; full `fmt/test/clippy` before `db6cb98`; full `fmt/test/clippy` before `16eb4ed`; full `fmt/test/clippy` before `8af350d`; full `fmt/test/clippy` before `203270f`; full `fmt/test/clippy` before `960bbb2`; full `fmt/test/clippy` before `72438c6`; full `fmt/test/clippy` before `16938e6`; admin settings targeted tests before `7424a47`; log state/filter targeted tests before `35f9b80` and `bae084b`; diagnostics route tests before `689a514`; quota warnings targeted tests before `b879dd6`; cooldown restore and Cloudflare Cookie clearing targeted tests before `0daa96b` and `efbbe8a` | Adds Axum router, health route, `/v1/models`, `/v1/models/catalog`, `/v1/models/{id}`, `/v1/models/{id}/info`, `/debug/models`, `/debug/diagnostics`, `/debug/fingerprint`, `/debug/upstream`, `/admin/diagnostics`, `/admin/auth/status`, `/admin/auth/logout`, `/admin/auth/login-start`, `/admin/auth/code-relay`, `/admin/auth/callback`, `/auth/callback`, `/admin/auth/device-login`, `/admin/auth/device-poll/{device_code}`, `/admin/accounts`, `/admin/accounts/quota-warnings`, `/admin/accounts/export`, `/admin/accounts/batch-delete`, `/admin/accounts/batch-status`, `/admin/accounts/health-check`, `/admin/accounts/{id}`, `/admin/accounts/{id}/label`, `/admin/accounts/{id}/status`, `/admin/accounts/{id}/refresh`, `/admin/accounts/{id}/reset-usage`, `/admin/accounts/{id}/quota`, `/admin/accounts/{id}/cookies`, `/admin/accounts/import`, `/admin/accounts/import-cli`, `/admin/api-keys`, `/admin/api-keys/export`, `/admin/api-keys/import`, `/admin/api-keys/batch-delete`, `/admin/api-keys/{id}`, `/admin/api-keys/{id}/label`, `/admin/api-keys/{id}/status`, `/admin/logs`, `/admin/logs/state`, `/admin/logs/{id}`, `/admin/usage-stats`, `/admin/usage-stats/summary`, `/admin/settings`, `/admin/refresh-models`, `/v1/responses` HTTP SSE collection with output item/delta reconstruction plus `output_text` sync for non-streaming clients, default streaming Responses passthrough when `stream` is omitted, WebSocket-backed Responses for `previous_response_id`, explicit non-history WebSocket Responses via `use_websocket`/`useWebSocket`, non-streaming and HTTP SSE setup fallback to another imported account after 429/402/403 account-state failures, WebSocket setup fallback after classified handshake failures when no `previous_response_id` is present, Responses field parity for service tier, tool choice, parallel tool calls, text format, prompt cache key, include, client metadata, reasoning include default, and Codex context headers, plus a dedicated `/v1/chat/completions` route backed by imported Codex accounts with fallback for buffered non-streaming and client-streaming output conversion. `/v1` unauthorized and model errors remain OpenAI-compatible, cached backend model snapshots are used by `/v1/models*` when present, global request-id response headers are preserved, `gpt-5.5` remains the default/test model baseline, and `/admin/settings` supports admin-session-gated read/write access with lower camelCase envelopes. `/admin/auth/status` returns sanitized authenticated state, first account metadata, and pool summary without decrypting or returning tokens. `/admin/auth/logout` clears all account rows and the runtime pool while relying on SQLite foreign-key cascades for usage/cookies. `/admin/auth/login-start` creates a short-lived in-memory PKCE session and returns the OpenAI authorization URL; `/admin/auth/code-relay`, `/admin/auth/callback`, and the registered `/auth/callback` redirect path exchange returned authorization codes and import tokens through the same JWT-claim validated path. `/admin/auth/device-login` starts the OpenAI OAuth device-code flow, and `/admin/auth/device-poll/{device_code}` handles `authorization_pending`/`slow_down` states or imports returned tokens without returning secrets. `/admin/accounts` reads metadata only and does not decrypt tokens for list views. Manual `POST /admin/accounts` accepts only `token` and/or `refreshToken`, rejects invalid/expired JWTs and missing ChatGPT account claims, derives metadata from JWT claims, ignores caller metadata/status/label, exchanges refresh-token-only inputs through `AppState`'s OpenAI token refresher, preserves or rotates refresh tokens, stores secrets encrypted, synchronizes the runtime account pool, updates existing accounts by `chatgpt_account_id` + `chatgpt_user_id`, and never echoes tokens. `/admin/accounts/quota-warnings` returns active warning/critical quota windows computed from cached SQLite quota JSON without contacting upstream. `/admin/accounts/import` accepts native and Sub2API OpenAI OAuth account exports, returns `sourceFormat`, and ignores proxy-only metadata. `/admin/accounts/import-cli` reads Codex CLI `auth.json` from `$CODEX_HOME` or an explicit admin fixture path, uses the same validated JWT-claim account import path as manual add, ignores caller metadata, and returns only sanitized import counts. `/admin/accounts/export` returns native or Sub2API OpenAI OAuth exports without proxy fields. `/admin/accounts/{id}/label`, `/status`, `/refresh`, `/reset-usage`, `/quota`, delete, batch-delete, batch-status, and `/admin/accounts/health-check` mutate or inspect SQLite plus runtime account pool without returning secrets; health/quota call Codex backend usage endpoints through imported account tokens and classify invalid/quota/banned/Cloudflare responses into account state or cooldown. `/admin/accounts/{id}/cookies` stores/reads/deletes encrypted per-account browser cookies. `/admin/api-keys/{id}/label`, `/status`, delete, and batch-delete mutate local client key rows used by `/v1` auth; `/admin/api-keys/export` returns metadata only without plaintext/keyHash/pepper, and `/admin/api-keys/import` rotates metadata into new local keys with plaintext returned once. These are local auth utilities, not TS provider key pool parity. `/admin/refresh-models` fetches backend model lists through imported accounts only, stores plan snapshots, and syncs runtime model-plan allowlists. Still missing live Chat streaming transform. | -| Admin quota warnings | Completed | `b879dd6` | `cargo test --test admin_accounts_cookies_quota_test quota_warnings`; `cargo test --test admin_accounts_cookies_quota_test`; `cargo fmt --check`; `cargo test`; `cargo clippy --all-targets --all-features --locked -- -D warnings` | Adds `/admin/accounts/quota-warnings`, backed by cached SQLite quota snapshots and configured warning thresholds. The route is admin-session gated, returns lower camelCase admin envelopes, and does not call upstream Codex while listing active warnings. | +| HTTP routes | Partial | `ab0ad8f` + `3830b21` + `6179d44` + `cfec8ba` + `6341336` + `228febd` + `c2cf7a6` + `e9fdea0` + `ee0ed90` + `fdb9374` + `9ca5c7d` + `b9c30ba` + `350ffbe` + `3ecd5b6` + `3960a35` + `1738f24` + `0b6cc7e` + `f75db60` + `4b19846` + `db6cb98` + `16eb4ed` + `8af350d` + `203270f` + `960bbb2` + `72438c6` + `16938e6` + `869b6f8` + `e1f35e0` + `93cfee3` + `bfe561d` + `cf5ff0b` + `6d18d2c` + `a32a53c` + `1d6c301` + `a3b4d2b` + `7424a47` + `8645b93` + `35f9b80` + `4a91dea` + `924e6f8` + `bb143e7` + `689a514` + `b879dd6` + `0daa96b` + `efbbe8a` + `bae084b` | `cargo test v1_requires_client_api_key_not_admin_cookie`; `cargo test models_route_returns_openai_compatible_codex_model_list`; `cargo test --test admin_settings_route_test`; `cargo test --test model_catalog_test`; `cargo test --test admin_accounts_route_test`; `cargo test --test cli_auth_import_test`; `cargo test --test admin_client_keys_route_test`; `cargo test --test admin_usage_stats_route_test`; `cargo test --test admin_accounts_cookies_quota_test`; `cargo test --test v1_upstream_route_test`; `cargo test --test chat_completions_route_test`; `cargo test --test codex_websocket_test`; targeted HTTP SSE error tests before `bfe561d`; targeted local API key import/export tests before `cf5ff0b`; WebSocket fallback targeted route tests; auth status/logout targeted `cargo test --test admin_accounts_route_test`; device login targeted `cargo test --test admin_accounts_route_test admin_auth_device`, `cargo test --test oauth_refresh_test`, and `cargo test --test admin_accounts_route_test`; PKCE auth-flow targeted `cargo test --test admin_accounts_route_test admin_auth_`, `cargo test --test oauth_refresh_test`, and `cargo test --test admin_accounts_route_test`; full `cargo fmt --check && cargo test && cargo clippy --all-targets --all-features --locked -- -D warnings` before `b9c30ba` and `350ffbe`; Task 5 targeted verification before `3ecd5b6` and `3960a35`; full `fmt/test/clippy` before `1738f24`; full `fmt/test/clippy` plus real-file smoke before `0b6cc7e`; full `fmt/test/clippy` before `f75db60`; full `fmt/test/clippy` before `4b19846`; full `fmt/test/clippy` before `db6cb98`; full `fmt/test/clippy` before `16eb4ed`; full `fmt/test/clippy` before `8af350d`; full `fmt/test/clippy` before `203270f`; full `fmt/test/clippy` before `960bbb2`; full `fmt/test/clippy` before `72438c6`; full `fmt/test/clippy` before `16938e6`; admin settings targeted tests before `7424a47`; log state/filter targeted tests before `35f9b80` and `bae084b`; diagnostics route tests before `689a514`; quota warnings targeted tests before `b879dd6`; cooldown restore and Cloudflare Cookie clearing targeted tests before `0daa96b` and `efbbe8a` | Adds Axum router, health route, `/v1/models`, `/v1/models/catalog`, `/v1/models/{id}`, `/v1/models/{id}/info`, `/debug/models`, `/debug/diagnostics`, `/debug/fingerprint`, `/debug/upstream`, `/api/admin/diagnostics`, `/api/admin/auth/status`, `/api/admin/auth/logout`, `/api/admin/auth/login-start`, `/api/admin/auth/code-relay`, `/api/admin/auth/openai/callback`, `/auth/openai/callback`, `/api/admin/auth/device-login`, `/api/admin/auth/device-poll/{device_code}`, `/api/admin/accounts`, `/api/admin/accounts/quota-warnings`, `/api/admin/accounts/export`, `/api/admin/accounts/batch-delete`, `/api/admin/accounts/batch-status`, `/api/admin/accounts/health-check`, `/api/admin/accounts/{id}`, `/api/admin/accounts/{id}/label`, `/api/admin/accounts/{id}/status`, `/api/admin/accounts/{id}/refresh`, `/api/admin/accounts/{id}/reset-usage`, `/api/admin/accounts/{id}/quota`, `/api/admin/accounts/{id}/cookies`, `/api/admin/accounts/import`, `/api/admin/accounts/import-cli`, `/api/admin/api-keys`, `/api/admin/api-keys/export`, `/api/admin/api-keys/import`, `/api/admin/api-keys/batch-delete`, `/api/admin/api-keys/{id}`, `/api/admin/api-keys/{id}/label`, `/api/admin/api-keys/{id}/status`, `/api/admin/logs`, `/api/admin/logs/state`, `/api/admin/logs/{id}`, `/api/admin/usage-stats`, `/api/admin/usage-stats/summary`, `/api/admin/settings`, `/api/admin/refresh-models`, `/v1/responses` HTTP SSE collection with output item/delta reconstruction plus `output_text` sync for non-streaming clients, default streaming Responses passthrough when `stream` is omitted, WebSocket-backed Responses for `previous_response_id`, explicit non-history WebSocket Responses via `use_websocket`/`useWebSocket`, non-streaming and HTTP SSE setup fallback to another imported account after 429/402/403 account-state failures, WebSocket setup fallback after classified handshake failures when no `previous_response_id` is present, Responses field parity for service tier, tool choice, parallel tool calls, text format, prompt cache key, include, client metadata, reasoning include default, and Codex context headers, plus a dedicated `/v1/chat/completions` route backed by imported Codex accounts with fallback for buffered non-streaming and client-streaming output conversion. `/v1` unauthorized and model errors remain OpenAI-compatible, cached backend model snapshots are used by `/v1/models*` when present, global request-id response headers are preserved, `gpt-5.5` remains the default/test model baseline, and `/api/admin/settings` supports admin-session-gated read/write access with lower camelCase envelopes. `/api/admin/auth/status` returns sanitized authenticated state, first account metadata, and pool summary without decrypting or returning tokens. `/api/admin/auth/logout` clears all account rows and the runtime pool while relying on SQLite foreign-key cascades for usage/cookies. `/api/admin/auth/login-start` creates a short-lived in-memory PKCE session and returns the OpenAI authorization URL; `/api/admin/auth/code-relay`, `/api/admin/auth/openai/callback`, and the registered `/auth/openai/callback` redirect path exchange returned authorization codes and import tokens through the same JWT-claim validated path. `/api/admin/auth/device-login` starts the OpenAI OAuth device-code flow, and `/api/admin/auth/device-poll/{device_code}` handles `authorization_pending`/`slow_down` states or imports returned tokens without returning secrets. `/api/admin/accounts` reads metadata only and does not decrypt tokens for list views. Manual `POST /api/admin/accounts` accepts only `token` and/or `refreshToken`, rejects invalid/expired JWTs and missing ChatGPT account claims, derives metadata from JWT claims, ignores caller metadata/status/label, exchanges refresh-token-only inputs through `AppState`'s OpenAI token refresher, preserves or rotates refresh tokens, stores secrets encrypted, synchronizes the runtime account pool, updates existing accounts by `chatgpt_account_id` + `chatgpt_user_id`, and never echoes tokens. `/api/admin/accounts/quota-warnings` returns active warning/critical quota windows computed from cached SQLite quota JSON without contacting upstream. `/api/admin/accounts/import` accepts native and Sub2API OpenAI OAuth account exports, returns `sourceFormat`, and ignores proxy-only metadata. `/api/admin/accounts/import-cli` reads Codex CLI `auth.json` from `$CODEX_HOME` or an explicit admin fixture path, uses the same validated JWT-claim account import path as manual add, ignores caller metadata, and returns only sanitized import counts. `/api/admin/accounts/export` returns native or Sub2API OpenAI OAuth exports without proxy fields. `/api/admin/accounts/{id}/label`, `/status`, `/refresh`, `/reset-usage`, `/quota`, delete, batch-delete, batch-status, and `/api/admin/accounts/health-check` mutate or inspect SQLite plus runtime account pool without returning secrets; health/quota call Codex backend usage endpoints through imported account tokens and classify invalid/quota/banned/Cloudflare responses into account state or cooldown. `/api/admin/accounts/{id}/cookies` stores/reads/deletes encrypted per-account browser cookies. `/api/admin/api-keys/{id}/label`, `/status`, delete, and batch-delete mutate local client key rows used by `/v1` auth; `/api/admin/api-keys/export` returns metadata only without plaintext/keyHash/pepper, and `/api/admin/api-keys/import` rotates metadata into new local keys with plaintext returned once. These are local auth utilities, not TS provider key pool parity. `/api/admin/refresh-models` fetches backend model lists through imported accounts only, stores plan snapshots, and syncs runtime model-plan allowlists. Still missing live Chat streaming transform. | +| Admin quota warnings | Completed | `b879dd6` | `cargo test --test admin_accounts_cookies_quota_test quota_warnings`; `cargo test --test admin_accounts_cookies_quota_test`; `cargo fmt --check`; `cargo test`; `cargo clippy --all-targets --all-features --locked -- -D warnings` | Adds `/api/admin/accounts/quota-warnings`, backed by cached SQLite quota snapshots and configured warning thresholds. The route is admin-session gated, returns lower camelCase admin envelopes, and does not call upstream Codex while listing active warnings. | | Admin/frontend response contract | Completed | `1c34ea7` + `b0c10f2` | `cargo test admin_envelope_serializes_lower_camel_case_request_id`; `cargo test admin_logs_reject_missing_admin_session_cookie`; `cargo test v1_response_has_request_id_header_without_admin_body_field`; full `fmt/test/clippy` before commit | Adds reusable admin envelopes with body codes, lower camelCase JSON, paginated list shape, requestId bodies, admin session cookie enforcement, and keeps `/v1/*` OpenAI-compatible. | -| HTTP auth boundary | Completed | `cec4607` + `e9fdea0` | `cargo test --test http_auth_test`; `cargo test --test routes_responses_test`; `cargo test --test admin_api_keys_route_test`; `cargo test --test admin_logs_route_test`; `cargo fmt --check`; `cargo clippy --all-targets --all-features --locked -- -D warnings`; `cargo test` | Adds a real `src/http/auth.rs` boundary for client API key and admin session cookie parsing. `/v1/*` accepts only enabled local `Bearer cpr_...` client API keys verified from SQLite, admin session parsing ignores client API keys, and route handlers no longer own ad hoc credential parsing. | +| HTTP auth boundary | Completed | `cec4607` + `e9fdea0` | `cargo test --test http_auth_test`; `cargo test --test routes_responses_test`; `cargo test --test admin_client_keys_route_test`; `cargo test --test admin_logs_route_test`; `cargo fmt --check`; `cargo clippy --all-targets --all-features --locked -- -D warnings`; `cargo test` | Adds a real `src/http/auth.rs` boundary for client API key and admin session cookie parsing. `/v1/*` accepts only enabled local `Bearer cpr_...` client API keys verified from SQLite, admin session parsing ignores client API keys, and route handlers no longer own ad hoc credential parsing. | | Model catalog | Partial | `6341336` + `1738f24` + `f75db60` + `4b19846` | `cargo test --test model_catalog_test`; `cargo test model_catalog_route_returns_codex_metadata_without_alias_entries`; `cargo test --test storage_schema_test`; `cargo test --test routes_responses_test model_catalog_route_returns_cached_backend_models`; `cargo test --test admin_models_route_test`; full `fmt/test/clippy` before `1738f24`; full `fmt/test/clippy` before `f75db60`; full `fmt/test/clippy` before `4b19846` | Adds a native static Codex model catalog with `gpt-5.5` default, configured aliases, reasoning/service-tier suffix parsing including `none` and `minimal`, OpenAI-compatible `/v1/models`, extended catalog/detail/info routes, debug summary, SQLite-backed backend model snapshots by plan, cached backend catalog merge, model-to-plan allowlist derivation, and an admin refresh route that probes Codex model endpoints with one imported account per plan and syncs the runtime account-pool allowlist. Still missing full model metadata parity. | | Upstream lifecycle | Partial | `88cad87` + `225fbff` + `c2cf7a6` + `636fa79` + `ee0ed90` + `fdb9374` + `5bf012c` + `9ca5c7d` + `b9c30ba` + `350ffbe` + `3ecd5b6` + `3960a35` + `bfe561d` + `6d18d2c` + `0daa96b` + `efbbe8a` | `cargo test responses_route_rejects_non_codex_provider_models`; `cargo test v1_requires_client_api_key_not_admin_cookie`; `cargo test --test codex_sse_usage_test`; `cargo test --test codex_client_test`; `cargo test --test oauth_refresh_test`; `cargo test --test v1_upstream_route_test`; `cargo test --test v1_upstream_fallback_test v1_responses_should_`; `cargo test --test v1_upstream_fallback_test v1_responses_should_cool_down_cloudflare_403_and_retry_next_account`; `cargo test --test account_pool_startup_test`; `cargo test --test account_pool_startup_test app_state_should_restore_persisted_cooldowns_into_runtime_pool`; `cargo test --test codex_websocket_test`; `cargo test --test admin_accounts_route_test`; Task 5 targeted `cargo test --test v1_upstream_route_test`, `cargo test --test chat_completions_route_test`, `cargo test --test account_pool_scheduling_test`, and WebSocket fallback route tests; HTTP SSE error mapping targeted `cargo test --test v1_upstream_route_test v1_responses_non_stream_should_return_upstream_error` and `cargo test --test v1_upstream_route_test v1_responses_stream_should_passthrough`; full `cargo fmt --check && cargo test && cargo clippy --all-targets --all-features --locked -- -D warnings` before `b9c30ba` and `350ffbe` | `/v1` enforces client API key shape, rejects non-Codex provider models through the model catalog, returns OpenAI-compatible errors, includes reusable SSE/usage helpers, sends Codex Desktop HTTP SSE requests through imported accounts, sends WebSocket `response.create` requests when `previous_response_id` requires upstream history, keeps `previous_response_id` requests account-affine on classified WebSocket handshake failures for streaming and non-streaming clients, replays/captures encrypted Cookies, clears Cloudflare-blocked account Cookies, captures successful WebSocket rate-limit headers, forwards Codex request context headers, fetches Codex usage/quota through imported account tokens for admin health/quota operations, normalizes `service_tier: fast` to upstream `priority`, collects `response.completed`, reconstructs missing assistant output from `response.output_item.done` or text deltas, synchronizes `output_text` for non-streaming clients, maps non-streaming HTTP SSE `error` and `response.failed` terminal frames to OpenAI-compatible `upstream_error` bodies, passes through Responses streaming SSE by default while marking `error` and `response.failed` terminal frames as failed lifecycle logs, adapts Chat Completions responses/chunks, refreshes and retries once after upstream 401 when a refresh token is stored, parses Retry-After plus Codex reset metadata from upstream errors, records 429 request attempts, persists 429 quota cooldowns and Cloudflare cooldowns to SQLite, classifies upstream 429/402/403 into account cooldown/quota/banned/Cloudflare state changes, retries non-streaming `/v1/responses`, HTTP SSE `/v1/responses` setup, non-history WebSocket setup, and `/v1/chat/completions` with another imported account when available, records token usage, writes v1 lifecycle event logs, records refresh failure classification for upstream 401, and restores the account pool from SQLite on startup. Still missing richer OpenAI-compatible error mapping and live Chat streaming transform. | | Codex WebSocket boundary | Completed | `cc0516d` + `b9c30ba` + `350ffbe` + `6d18d2c` | `cargo test --test codex_websocket_test`; `cargo test --test v1_responses_websocket_test v1_responses_should_use_websocket_for_previous_response_id_streaming`; WebSocket route fallback tests; full `cargo fmt --check && cargo test && cargo clippy --all-targets --all-features --locked -- -D warnings` before `b9c30ba` and `350ffbe` | Adds `src/codex/transport/websocket.rs` with tested transport classification for HTTP SSE versus WebSocket-only requests. `previous_response_id` and explicit WebSocket requests use typed transport selection, local-only `use_websocket` is not serialized into upstream JSON, WebSocket `response.create` carries Codex headers and parity fields, incoming WebSocket messages are emitted as SSE-compatible events, handshake HTTP failures surface as upstream errors before request body send, successful handshake headers capture turn state, cookies, and rate-limit headers, non-history WebSocket setup can fallback to another imported account, and `previous_response_id` WebSocket failures remain on the original account. | diff --git a/docs/rust-community-review.md b/docs/rust-community-review.md index ac314a44a..87ed6187a 100644 --- a/docs/rust-community-review.md +++ b/docs/rust-community-review.md @@ -37,7 +37,7 @@ **问题:** ``` -1697 行 - src/http/admin/accounts.rs ⚠️ 太大! +1697 行 - src/http/api/admin/accounts.rs ⚠️ 太大! 895 行 - src/codex/upstream/mod.rs 784 行 - src/codex/accounts/repository.rs ``` @@ -46,9 +46,9 @@ **改进方案:** ```rust -// 当前:src/http/admin/accounts.rs (1697 行) +// 当前:src/http/api/admin/accounts.rs (1697 行) // 建议拆分为: -src/http/admin/accounts/ +src/http/api/admin/accounts/ ├── mod.rs // 路由注册 ├── list.rs // GET /accounts ├── create.rs // POST /accounts @@ -138,7 +138,7 @@ src/codex/accounts/service/ ├── cookies.rs // Cookie 操作 ├── health.rs // 健康检查 ├── import.rs // 导入逻辑 -├── mutation.rs // CRUD +├── lifecycle.rs // CRUD ├── quota.rs // 配额 ├── refresh.rs // 刷新 └── runtime_pool.rs // 池同步 @@ -397,7 +397,7 @@ pub account_pool: Arc<RwLock<AccountPool>>, - 风险:低 2. **🟡 拆分大文件** - - 先拆 `http/admin/accounts.rs` (1697 行) + - 先拆 `http/api/admin/accounts.rs` (1697 行) - 按功能模块拆分(list/create/update/delete/oauth) - 成本:中等 - 收益:提升可维护性 diff --git a/docs/status-codes.md b/docs/status-codes.md index c722d7eef..c7143d4f2 100644 --- a/docs/status-codes.md +++ b/docs/status-codes.md @@ -12,9 +12,9 @@ HTTP status codes are not replaced by body codes. Admin/frontend APIs return bot | HTTP status | Body code range | Meaning | | --- | --- | --- | -| `200` | `200` | Successful read, login, or mutation with response body. | +| `200` | `200` | Successful read, login, or lifecycle with response body. | | `201` | `201` | Resource created. | -| `204` | `204` | Successful mutation with no response body. No JSON envelope is sent. | +| `204` | `204` | Successful lifecycle with no response body. No JSON envelope is sent. | | `400` | `40000`-`40099` | Malformed JSON, invalid parameters, or invalid cursor. | | `401` | `40100`-`40199` | Missing/expired admin session or bad admin password. | | `403` | `40300`-`40399` | Authenticated admin lacks permission for a local-only/bootstrap action. | diff --git a/docs/superpowers/plans/2026-06-14-backend-naming-architecture.md b/docs/superpowers/plans/2026-06-14-backend-naming-architecture.md new file mode 100644 index 000000000..8d06fba35 --- /dev/null +++ b/docs/superpowers/plans/2026-06-14-backend-naming-architecture.md @@ -0,0 +1,214 @@ +# Backend Naming Architecture Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Rename backend modules so every directory and file has one clear domain meaning while reserving `/admin` for the future Web console. + +**Architecture:** Move the current management JSON handlers from `admin/http` to `admin/api`, keep `/v1/*` under `codex/serving`, add a `web` backend shell for future static assets, and split ambiguous support modules into domain-specific names. The work is mechanical and must not change upstream Codex behavior. + +**Tech Stack:** Rust 2021, Axum, SQLx, Tokio, tracing, existing integration tests. + +--- + +## File Structure + +Create: +- `src/web/mod.rs` +- `src/web/router.rs` +- `src/web/assets.rs` +- `src/web/shell.rs` +- `src/web/security.rs` +- `src/platform/crypto/mod.rs` +- `src/platform/crypto/secret_box.rs` +- `src/platform/logging/mod.rs` +- `src/admin/api/client_keys/mod.rs` +- `src/admin/api/logs/mod.rs` +- `src/admin/session/mod.rs` +- `src/admin/client_keys/mod.rs` +- `src/codex/events/mod.rs` +- `src/codex/models/mod.rs` + +Move or rename: +- `src/admin/http` to `src/admin/api` +- `src/admin/auth/repository.rs` to `src/admin/session/repository.rs` +- `src/admin/auth/service.rs` to `src/admin/session/service.rs` +- `src/admin/auth/api_key.rs` to `src/admin/client_keys/service.rs` +- `src/admin/http/api_keys.rs` to `src/admin/api/client_keys/mod.rs` +- `src/admin/http/auth.rs` to `src/admin/api/session.rs` +- `src/admin/http/logs.rs` to `src/admin/api/logs/mod.rs` +- `src/admin/http/accounts/mutate.rs` to `src/admin/api/accounts/lifecycle.rs` +- `src/codex/accounts/cf_path_block.rs` to `src/codex/accounts/cloudflare_challenge.rs` +- `src/codex/accounts/models` to `src/codex/models` +- `src/codex/accounts/repository/lease.rs` to `src/codex/accounts/repository/leases.rs` +- `src/codex/accounts/repository/quota.rs` to `src/codex/accounts/repository/quotas.rs` +- `src/codex/accounts/repository/token.rs` to `src/codex/accounts/repository/tokens.rs` +- `src/codex/accounts/service/mutation.rs` to `src/codex/accounts/service/lifecycle.rs` +- `src/codex/accounts/service/runtime_pool.rs` to `src/codex/accounts/service/pool_sync.rs` +- `src/codex/gateway/identity.rs` to `src/codex/gateway/conversation_identity.rs` +- `src/codex/gateway/installation.rs` to `src/codex/gateway/installation_id.rs` +- `src/codex/gateway/oauth/cli_import.rs` to `src/codex/gateway/oauth/codex_cli.rs` +- `src/codex/gateway/transport/client.rs` to `src/codex/gateway/transport/http_client.rs` +- `src/codex/gateway/transport/usage.rs` to `src/codex/gateway/transport/usage_events.rs` +- `src/codex/logs` to `src/codex/events` +- `src/codex/logs/rotation.rs` to `src/platform/logging/rotation.rs` +- `src/codex/tasks/model.rs` to `src/codex/tasks/model_refresh.rs` +- `src/codex/tasks/quota.rs` to `src/codex/tasks/quota_refresh.rs` +- `src/codex/tasks/refresh.rs` to `src/codex/tasks/token_refresh.rs` +- `src/codex/serving/dispatch/audit.rs` to `src/codex/serving/dispatch/stream_audit.rs` +- `src/codex/serving/dispatch/refresh.rs` to `src/codex/serving/dispatch/account_refresh.rs` +- `src/platform/crypto.rs` to `src/platform/crypto/secret_box.rs` +- `src/platform/http/middleware.rs` to `src/platform/http/request_id.rs` +- `src/platform/identity/api_key.rs` to `src/platform/identity/client_key.rs` +- `src/platform/identity/api_key_repository.rs` to `src/platform/identity/client_key_repository.rs` + +## Task 1: Freeze the naming contract + +**Files:** +- Create: `docs/superpowers/specs/2026-06-14-backend-naming-architecture-design.md` +- Create: `docs/superpowers/plans/2026-06-14-backend-naming-architecture.md` + +- [x] **Step 1: Write the design spec** + +Record route boundaries, target source tree, naming decisions, and migration policy. + +- [x] **Step 2: Write this implementation plan** + +List every file move and the verification gates. + +## Task 2: Move management API and backend Web shell + +**Files:** +- Move: `src/admin/http/**` to `src/admin/api/**` +- Create: `src/web/{mod.rs,router.rs,assets.rs,shell.rs,security.rs}` +- Modify: `src/admin/mod.rs` +- Modify: `src/runtime/router.rs` +- Test: `tests/architecture/admin_boundary.rs` + +- [x] **Step 1: Move the management API directory** + +Run file moves so handlers live under `admin::api`. + +- [x] **Step 2: Update admin module exports** + +Expose `pub mod api;`, `pub mod session;`, and `pub mod client_keys;`. + +- [x] **Step 3: Add empty Web shell modules** + +Add `src/web` modules with module-level documentation only. Do not serve frontend assets yet. + +- [x] **Step 4: Update route mounting** + +Import `crate::admin::api as admin_api` in `runtime/router.rs` and merge `admin_api::router()`. + +## Task 3: Rename session and client key domains + +**Files:** +- Move: `src/admin/auth/service.rs` to `src/admin/session/service.rs` +- Move: `src/admin/auth/repository.rs` to `src/admin/session/repository.rs` +- Move: `src/admin/auth/api_key.rs` to `src/admin/client_keys/service.rs` +- Move: `src/admin/api/api_keys.rs` to `src/admin/api/client_keys/mod.rs` +- Move: `src/admin/api/auth.rs` to `src/admin/api/session.rs` +- Modify: `src/runtime/state.rs` +- Modify: `src/runtime/bootstrap.rs` +- Test: `tests/architecture/admin_boundary.rs` + +- [x] **Step 1: Move files into the new domains** + +Keep type names stable unless a rename is required by compiler errors. + +- [x] **Step 2: Replace imports** + +Replace `admin::auth::service` with `admin::session::service`, `admin::auth::repository` with `admin::session::repository`, and `admin::auth::api_key` with `admin::client_keys::service`. + +- [x] **Step 3: Update API router imports** + +Import the API key handlers from `admin::api::client_keys` and session handlers from `admin::api::session`. + +## Task 4: Rename Codex catalog, events, gateway, transport, and task modules + +**Files:** +- Move the Codex files listed in the File Structure section. +- Modify: `src/codex/mod.rs` +- Modify: `src/codex/accounts/mod.rs` +- Modify: `src/codex/accounts/repository.rs` +- Modify: `src/codex/accounts/service/mod.rs` +- Modify: `src/codex/gateway/mod.rs` +- Modify: `src/codex/gateway/oauth/mod.rs` +- Modify: `src/codex/gateway/transport/mod.rs` +- Modify: `src/codex/tasks/mod.rs` +- Modify: `src/codex/serving/dispatch/mod.rs` +- Test: `tests/architecture/{accounts_boundary.rs,gateway_boundary.rs,serving_boundary.rs}` + +- [x] **Step 1: Move model catalog to `codex/models`** + +Update references from `codex::accounts::models` to `codex::models`. + +- [x] **Step 2: Move business logs to `codex/events`** + +Update references from `codex::logs` to `codex::events`. + +- [x] **Step 3: Move gateway and transport files** + +Update `identity`, `installation`, `cli_import`, `client`, and `usage` references to the new names. + +- [x] **Step 4: Move task and dispatch helper names** + +Update `model`, `quota`, `refresh`, `audit`, and dispatch refresh references to the new names. + +## Task 5: Rename platform foundations + +**Files:** +- Move: `src/platform/crypto.rs` to `src/platform/crypto/secret_box.rs` +- Move: `src/platform/http/middleware.rs` to `src/platform/http/request_id.rs` +- Move: `src/platform/identity/api_key.rs` to `src/platform/identity/client_key.rs` +- Move: `src/platform/identity/api_key_repository.rs` to `src/platform/identity/client_key_repository.rs` +- Modify: `src/platform/mod.rs` +- Modify: `src/platform/http/mod.rs` +- Modify: `src/platform/identity/mod.rs` +- Test: `tests/architecture/platform_boundary.rs` + +- [x] **Step 1: Move files** + +Create `platform::crypto::secret_box`, re-export `SecretBox`, `CryptoError`, and `CryptoResult` from `platform::crypto`. + +- [x] **Step 2: Replace imports** + +Update all imports of `platform::http::middleware`, `platform::identity::api_key`, and `platform::identity::api_key_repository`. + +## Task 6: Update tests, docs references, and verify + +**Files:** +- Modify tests under `tests/**` +- Modify current docs under `docs/api.md`, `docs/architecture-capability-plan.md`, `docs/architecture-reorganization.md`, `docs/implementation-status.md`, and this spec/plan if paths drift. + +- [x] **Step 1: Update architecture tests** + +Make architecture tests assert the new public module paths. + +- [x] **Step 2: Update integration test imports** + +Replace old paths with new paths while leaving test behavior unchanged. + +- [x] **Step 3: Run formatting** + +Run: `cargo fmt --check` + +Expected: no formatting diff. If it fails, run `cargo fmt` and re-run `cargo fmt --check`. + +- [x] **Step 4: Run tests** + +Run: `cargo test --locked` + +Expected: all tests pass. + +- [x] **Step 5: Run Clippy** + +Run: `cargo clippy --all-targets --all-features --locked -- -D warnings` + +Expected: no warnings. + +- [x] **Step 6: Check whitespace** + +Run: `git diff --check` + +Expected: no whitespace errors. diff --git a/docs/superpowers/specs/2026-06-14-backend-naming-architecture-design.md b/docs/superpowers/specs/2026-06-14-backend-naming-architecture-design.md new file mode 100644 index 000000000..8be8c8a2a --- /dev/null +++ b/docs/superpowers/specs/2026-06-14-backend-naming-architecture-design.md @@ -0,0 +1,97 @@ +# Backend Naming Architecture Design + +## Goal + +将后端目录整理成一名一义的结构,为 `/admin` Web 控制台预留承载面,并把现有管理 JSON API 收束到 `/api/admin` 的后端契约层。 + +## Scope + +本次设计只覆盖 Rust 后端目录、模块命名、路由边界和测试命名。前端工程由独立目录和独立约定承载,后端只提供 `src/web` 作为构建产物的挂载位置。 + +## Route Boundaries + +```text +/admin/* Web 控制台入口,由 src/web 承载 +/api/admin/* 管理 API,由 src/admin/api 承载 +/v1/* OpenAI-compatible API,由 src/codex/serving 承载 +/auth/openai/callback OpenAI OAuth 回调 +/debug/* 本地诊断 +/health 存活检查 +``` + +`admin` 是管理域,`web` 是浏览器承载面,`codex` 是 Codex 上游内核,`platform` 是跨领域系统能力。任何目录名都应表达唯一责任,避免 `http`、`auth`、`api_key` 这类多义名泄漏到领域边界。 + +## Target Source Tree + +```text +src/ + main.rs + lib.rs + + runtime/{mod.rs, bootstrap.rs, router.rs, state.rs} + runtime/tasks/{mod.rs, coordinator.rs, types.rs} + + web/{mod.rs, router.rs, assets.rs, shell.rs, security.rs} + + admin/{mod.rs, settings.rs} + admin/api/{mod.rs, router.rs, response.rs, session.rs, diagnostics.rs, settings.rs, models.rs, usage.rs} + admin/api/accounts/{mod.rs, list.rs, create.rs, import.rs, export.rs, lifecycle.rs, quota.rs, cookies.rs, oauth.rs, health.rs} + admin/api/client_keys/{mod.rs, list.rs, create.rs, import.rs, export.rs, lifecycle.rs} + admin/api/logs/{mod.rs, query.rs, detail.rs, state.rs} + admin/session/{mod.rs, service.rs, repository.rs} + admin/client_keys/{mod.rs, service.rs} + admin/tasks/{mod.rs, session_cleanup.rs} + + codex/{mod.rs} + codex/accounts/{mod.rs, model.rs, jwt.rs, pool.rs, lifecycle.rs, cloudflare_challenge.rs, usage_snapshots.rs} + codex/accounts/cookies/{mod.rs, jar.rs, repository.rs} + codex/accounts/repository/{mod.rs, accounts.rs, tokens.rs, leases.rs, quotas.rs, usage.rs} + codex/accounts/service/{mod.rs, cookies.rs, health.rs, import.rs, lifecycle.rs, quota.rs, refresh.rs, pool_sync.rs} + + codex/models/{mod.rs, catalog.rs, repository.rs, service.rs} + + codex/gateway/{mod.rs, conversation_identity.rs, installation_id.rs} + codex/gateway/fingerprint/{mod.rs, model.rs, repository.rs, update_checker.rs, updater.rs} + codex/gateway/oauth/{mod.rs, client.rs, codex_cli.rs, refresh.rs, token.rs} + codex/gateway/protocol/{mod.rs, schema.rs, error.rs, openai_to_codex.rs, codex_to_openai.rs} + codex/gateway/transport/{mod.rs, http_client.rs, headers.rs, rate_limits.rs, sse.rs, usage_events.rs} + codex/gateway/transport/websocket/{mod.rs, codec.rs, pool.rs} + + codex/events/{mod.rs, event.rs, repository.rs, service.rs} + codex/usage/{mod.rs, service.rs} + codex/tasks/{mod.rs, model_refresh.rs, quota_refresh.rs, token_refresh.rs} + + codex/serving/{mod.rs, chat.rs, responses.rs, diagnostics.rs} + codex/serving/http/{mod.rs, router.rs, auth.rs, chat.rs, responses.rs, models.rs, diagnostics.rs, errors.rs} + codex/serving/dispatch/{mod.rs, affinity.rs, routing.rs, fallback.rs, limits.rs, account_refresh.rs, stream.rs, stream_audit.rs, usage.rs} + + platform/{mod.rs} + platform/crypto/{mod.rs, secret_box.rs} + platform/http/{mod.rs, auth.rs, health.rs, request_id.rs} + platform/identity/{mod.rs, admin_session.rs, client_key.rs, client_key_repository.rs, error.rs} + platform/storage/{mod.rs, db.rs, paths.rs, schema.sql} + platform/logging/{mod.rs, rotation.rs} + + config/{mod.rs, loader.rs, types.rs} + utils/{mod.rs, json.rs, pagination.rs} +``` + +## Naming Decisions + +`admin/http` becomes `admin/api` because the module exposes the management API contract, not generic HTTP utilities. + +`admin/auth` splits into `admin/session` and `admin/client_keys`. Admin login is session management; local `cpr_` keys are client credentials for `/v1/*`, not upstream provider keys. + +`api_keys` becomes `client_keys` where the code manages local client credentials. The route can remain `/api/admin/api-keys` for product compatibility, but Rust modules should state the stronger domain meaning. + +`codex/accounts/models` moves to `codex/models`. Model catalog and plan snapshots are Codex catalog concerns, not account-owned state. + +`codex/logs` becomes `codex/events`. The SQLite event stream is a business event store used by the admin console; process file logging belongs under `platform/logging`. + +`platform/http/middleware.rs` becomes `platform/http/request_id.rs` because it has one stable responsibility. + +## Migration Policy + +Implementation should be mechanical first: move files, update module declarations, update imports, then run formatting and tests. Business logic changes are out of scope unless required to preserve existing behavior under the new route boundary. + +The migration must keep `/v1/*` OpenAI-compatible behavior unchanged and keep admin session auth separate from local client API keys. diff --git a/src/admin/http/accounts/cookies.rs b/src/admin/api/accounts/cookies.rs similarity index 98% rename from src/admin/http/accounts/cookies.rs rename to src/admin/api/accounts/cookies.rs index 6724f10ec..4ad59882c 100644 --- a/src/admin/http/accounts/cookies.rs +++ b/src/admin/api/accounts/cookies.rs @@ -7,7 +7,7 @@ use axum::{ use serde::{Deserialize, Serialize}; use serde_json::Value; -use crate::{platform::http::middleware::RequestId, runtime::state::AppState}; +use crate::{platform::http::request_id::RequestId, runtime::state::AppState}; use super::super::{require_admin_session, AdminEnvelope, AdminError, AdminResponse}; use super::account_service_error; diff --git a/src/admin/api/accounts/create.rs b/src/admin/api/accounts/create.rs new file mode 100644 index 000000000..383b90b41 --- /dev/null +++ b/src/admin/api/accounts/create.rs @@ -0,0 +1,42 @@ +use axum::{ + extract::State, + http::{HeaderMap, StatusCode}, + response::IntoResponse, + Extension, Json, +}; +use serde::Deserialize; + +use crate::{platform::http::request_id::RequestId, runtime::state::AppState}; + +use super::super::{require_admin_session, AdminEnvelope, AdminError, AdminResponse}; +use super::{admin_account_data_from_stored, validated_account_import_error}; + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CreateAccountRequest { + pub token: Option<String>, + pub refresh_token: Option<String>, +} + +pub async fn create_account( + State(state): State<AppState>, + Extension(request_id): Extension<RequestId>, + headers: HeaderMap, + Json(payload): Json<CreateAccountRequest>, +) -> Result<impl IntoResponse, AdminError> { + let request_id = request_id.as_str().to_string(); + require_admin_session(&state, &headers, &request_id).await?; + + let stored = state + .services + .accounts + .import_validated(payload.token, payload.refresh_token) + .await + .map_err(|error| validated_account_import_error(error, &request_id))?; + + // 手动添加账号的响应只返回可展示元数据,OAuth token 永不回显。 + Ok(AdminResponse::new( + StatusCode::OK, + AdminEnvelope::ok(admin_account_data_from_stored(stored), request_id), + )) +} diff --git a/src/admin/http/accounts/delete.rs b/src/admin/api/accounts/delete.rs similarity index 97% rename from src/admin/http/accounts/delete.rs rename to src/admin/api/accounts/delete.rs index ad125ec52..03f4d2b82 100644 --- a/src/admin/http/accounts/delete.rs +++ b/src/admin/api/accounts/delete.rs @@ -6,7 +6,7 @@ use axum::{ }; use serde::{Deserialize, Serialize}; -use crate::{platform::http::middleware::RequestId, runtime::state::AppState}; +use crate::{platform::http::request_id::RequestId, runtime::state::AppState}; use super::super::{require_admin_session, AdminEnvelope, AdminError, AdminResponse}; use super::account_service_error; diff --git a/src/admin/http/accounts/list.rs b/src/admin/api/accounts/export.rs similarity index 79% rename from src/admin/http/accounts/list.rs rename to src/admin/api/accounts/export.rs index e81f4b397..03f5298f4 100644 --- a/src/admin/http/accounts/list.rs +++ b/src/admin/api/accounts/export.rs @@ -10,23 +10,12 @@ use serde::Deserialize; use serde_json::{json, Map, Value}; use crate::{ - codex::accounts::repository::StoredAccount, - platform::http::middleware::RequestId, + codex::accounts::repository::StoredAccount, platform::http::request_id::RequestId, runtime::state::AppState, - utils::pagination::{clamp_limit, Page}, }; -use super::super::{ - require_admin_session, AdminEnvelope, AdminError, AdminPageEnvelope, AdminResponse, -}; -use super::{account_export_ids, account_service_error, account_status_value, AdminAccountData}; - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct AccountsQuery { - pub cursor: Option<String>, - pub limit: Option<u32>, -} +use super::super::{require_admin_session, AdminEnvelope, AdminError, AdminResponse}; +use super::{account_export_ids, account_service_error, account_status_value}; #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] @@ -41,34 +30,6 @@ enum AccountExportFormat { Sub2Api, } -pub async fn accounts( - State(state): State<AppState>, - Extension(request_id): Extension<RequestId>, - headers: HeaderMap, - Query(query): Query<AccountsQuery>, -) -> Result<impl IntoResponse, AdminError> { - let request_id = request_id.as_str().to_string(); - require_admin_session(&state, &headers, &request_id).await?; - - let limit = clamp_limit(query.limit.unwrap_or(50)); - let page = state - .services - .accounts - .list(query.cursor, limit) - .await - .map_err(|error| account_service_error(error, &request_id))?; - let Page { items, next_cursor } = page; - let page = Page { - items: items.into_iter().map(AdminAccountData::from).collect(), - next_cursor, - }; - - Ok(AdminResponse::new( - StatusCode::OK, - AdminPageEnvelope::ok(page, limit, request_id), - )) -} - pub async fn export_accounts( State(state): State<AppState>, Extension(request_id): Extension<RequestId>, diff --git a/src/admin/http/accounts/health.rs b/src/admin/api/accounts/health.rs similarity index 99% rename from src/admin/http/accounts/health.rs rename to src/admin/api/accounts/health.rs index bef7d262f..65573823c 100644 --- a/src/admin/http/accounts/health.rs +++ b/src/admin/api/accounts/health.rs @@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize}; use crate::{ codex::accounts::service::{AccountProbeResult, HealthCheckError}, - platform::http::middleware::RequestId, + platform::http::request_id::RequestId, runtime::state::AppState, }; diff --git a/src/admin/http/accounts/create.rs b/src/admin/api/accounts/import.rs similarity index 92% rename from src/admin/http/accounts/create.rs rename to src/admin/api/accounts/import.rs index 62479b0fb..a28223860 100644 --- a/src/admin/http/accounts/create.rs +++ b/src/admin/api/accounts/import.rs @@ -13,13 +13,13 @@ use crate::{ accounts::service::{AccountImportEntry, StoreImportAccountError}, gateway::oauth::{default_codex_home, read_cli_auth_from_home}, }, - platform::http::middleware::RequestId, + platform::http::request_id::RequestId, runtime::state::AppState, utils::json::{first_string, string_at}, }; use super::super::{require_admin_session, AdminEnvelope, AdminError, AdminResponse}; -use super::{admin_account_data_from_stored, validated_account_import_error}; +use super::validated_account_import_error; #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum AccountImportFormat { @@ -52,42 +52,12 @@ pub struct AccountImportData { pub source_format: String, } -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CreateAccountRequest { - pub token: Option<String>, - pub refresh_token: Option<String>, -} - #[derive(Debug, Default, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ImportCliAuthRequest { pub codex_home: Option<String>, } -pub async fn create_account( - State(state): State<AppState>, - Extension(request_id): Extension<RequestId>, - headers: HeaderMap, - Json(payload): Json<CreateAccountRequest>, -) -> Result<impl IntoResponse, AdminError> { - let request_id = request_id.as_str().to_string(); - require_admin_session(&state, &headers, &request_id).await?; - - let stored = state - .services - .accounts - .import_validated(payload.token, payload.refresh_token) - .await - .map_err(|error| validated_account_import_error(error, &request_id))?; - - // 手动添加账号的响应只返回可展示元数据,OAuth token 永不回显。 - Ok(AdminResponse::new( - StatusCode::OK, - AdminEnvelope::ok(admin_account_data_from_stored(stored), request_id), - )) -} - pub async fn import_accounts( State(state): State<AppState>, Extension(request_id): Extension<RequestId>, diff --git a/src/admin/http/accounts/mutate.rs b/src/admin/api/accounts/lifecycle.rs similarity index 99% rename from src/admin/http/accounts/mutate.rs rename to src/admin/api/accounts/lifecycle.rs index beac8bf13..f001f77c4 100644 --- a/src/admin/http/accounts/mutate.rs +++ b/src/admin/api/accounts/lifecycle.rs @@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize}; use crate::{ codex::accounts::{model::AccountStatus, service::RefreshAccountError}, - platform::http::middleware::RequestId, + platform::http::request_id::RequestId, runtime::state::AppState, }; diff --git a/src/admin/api/accounts/list.rs b/src/admin/api/accounts/list.rs new file mode 100644 index 000000000..488111e90 --- /dev/null +++ b/src/admin/api/accounts/list.rs @@ -0,0 +1,51 @@ +use axum::{ + extract::{Query, State}, + http::{HeaderMap, StatusCode}, + response::IntoResponse, + Extension, +}; +use serde::Deserialize; + +use crate::{ + platform::http::request_id::RequestId, + runtime::state::AppState, + utils::pagination::{clamp_limit, Page}, +}; + +use super::super::{require_admin_session, AdminError, AdminPageEnvelope, AdminResponse}; +use super::{account_service_error, AdminAccountData}; + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AccountsQuery { + pub cursor: Option<String>, + pub limit: Option<u32>, +} + +pub async fn accounts( + State(state): State<AppState>, + Extension(request_id): Extension<RequestId>, + headers: HeaderMap, + Query(query): Query<AccountsQuery>, +) -> Result<impl IntoResponse, AdminError> { + let request_id = request_id.as_str().to_string(); + require_admin_session(&state, &headers, &request_id).await?; + + let limit = clamp_limit(query.limit.unwrap_or(50)); + let page = state + .services + .accounts + .list(query.cursor, limit) + .await + .map_err(|error| account_service_error(error, &request_id))?; + let Page { items, next_cursor } = page; + let page = Page { + items: items.into_iter().map(AdminAccountData::from).collect(), + next_cursor, + }; + + Ok(AdminResponse::new( + StatusCode::OK, + AdminPageEnvelope::ok(page, limit, request_id), + )) +} diff --git a/src/admin/http/accounts/mod.rs b/src/admin/api/accounts/mod.rs similarity index 97% rename from src/admin/http/accounts/mod.rs rename to src/admin/api/accounts/mod.rs index f5802e405..ba3aafbf7 100644 --- a/src/admin/http/accounts/mod.rs +++ b/src/admin/api/accounts/mod.rs @@ -14,9 +14,11 @@ use super::AdminError; pub mod cookies; pub mod create; pub mod delete; +pub mod export; pub mod health; +pub mod import; +pub mod lifecycle; pub mod list; -pub mod mutate; pub mod oauth; pub mod quota; @@ -24,22 +26,21 @@ pub use cookies::{ delete_account_cookies, get_account_cookies, set_account_cookies, AccountCookiesData, DeleteAccountCookiesData, SetAccountCookiesRequest, }; -pub use create::{ - create_account, import_accounts, import_cli_auth, AccountImportData, CreateAccountRequest, - ImportCliAuthRequest, -}; +pub use create::{create_account, CreateAccountRequest}; pub use delete::{ batch_delete_accounts, delete_account, BatchDeleteAccountsData, BatchDeleteAccountsRequest, DeleteAccountData, }; +pub use export::{export_accounts, AccountExportQuery}; pub use health::{health_check_accounts, AccountProbeData, HealthCheckData, HealthCheckRequest}; -pub use list::{accounts, export_accounts, AccountExportQuery, AccountsQuery}; -pub use mutate::{ +pub use import::{import_accounts, import_cli_auth, AccountImportData, ImportCliAuthRequest}; +pub use lifecycle::{ batch_update_account_status, refresh_account, reset_account_usage, update_account_label, update_account_status, BatchUpdateAccountStatusData, BatchUpdateAccountStatusRequest, ResetAccountUsageData, UpdateAccountLabelData, UpdateAccountLabelRequest, UpdateAccountStatusData, UpdateAccountStatusRequest, }; +pub use list::{accounts, AccountsQuery}; pub use oauth::{ auth_callback, auth_code_relay, auth_device_login, auth_device_poll, auth_login_start, AdminAuthCallbackQuery, AdminAuthCodeRelayData, AdminAuthCodeRelayRequest, diff --git a/src/admin/http/accounts/oauth.rs b/src/admin/api/accounts/oauth.rs similarity index 98% rename from src/admin/http/accounts/oauth.rs rename to src/admin/api/accounts/oauth.rs index 1d70f58d9..bd79cd1f1 100644 --- a/src/admin/http/accounts/oauth.rs +++ b/src/admin/api/accounts/oauth.rs @@ -11,10 +11,10 @@ use reqwest::Url; use serde::{Deserialize, Serialize}; use crate::{ - admin::auth::service::{AdminAuthOAuthError, AdminAuthPkceExchange}, + admin::session::service::{AdminAuthOAuthError, AdminAuthPkceExchange}, codex::gateway::oauth::{DeviceCode, OAuthError}, config::AppConfig, - platform::http::middleware::RequestId, + platform::http::request_id::RequestId, runtime::state::AppState, }; diff --git a/src/admin/http/accounts/quota.rs b/src/admin/api/accounts/quota.rs similarity index 99% rename from src/admin/http/accounts/quota.rs rename to src/admin/api/accounts/quota.rs index 605ff6b41..674a88455 100644 --- a/src/admin/http/accounts/quota.rs +++ b/src/admin/api/accounts/quota.rs @@ -13,7 +13,7 @@ use crate::{ AccountQuotaError, AccountQuotaWarning as ServiceAccountQuotaWarning, AccountQuotaWarnings as ServiceAccountQuotaWarnings, }, - platform::http::middleware::RequestId, + platform::http::request_id::RequestId, runtime::state::AppState, }; diff --git a/src/admin/api/client_keys/create.rs b/src/admin/api/client_keys/create.rs new file mode 100644 index 000000000..6769c0739 --- /dev/null +++ b/src/admin/api/client_keys/create.rs @@ -0,0 +1,28 @@ +use axum::{ + extract::State, http::HeaderMap, http::StatusCode, response::IntoResponse, Extension, Json, +}; + +use crate::{platform::http::request_id::RequestId, runtime::state::AppState}; + +use super::{api_key_service_error, CreateApiKeyRequest, CreatedClientApiKeyData}; +use crate::admin::api::{require_admin_session, AdminEnvelope, AdminError, AdminResponse}; + +pub async fn create_api_key( + State(state): State<AppState>, + Extension(request_id): Extension<RequestId>, + headers: HeaderMap, + Json(payload): Json<CreateApiKeyRequest>, +) -> Result<impl IntoResponse, AdminError> { + let request_id = request_id.as_str().to_string(); + require_admin_session(&state, &headers, &request_id).await?; + match state.services.api_keys.create(&payload.name).await { + Ok(created) => Ok(AdminResponse::new( + StatusCode::OK, + AdminEnvelope::ok( + CreatedClientApiKeyData::new(created.key, created.plaintext), + request_id, + ), + )), + Err(error) => Err(api_key_service_error(error, request_id)), + } +} diff --git a/src/admin/api/client_keys/export.rs b/src/admin/api/client_keys/export.rs new file mode 100644 index 000000000..5a0bf0b8e --- /dev/null +++ b/src/admin/api/client_keys/export.rs @@ -0,0 +1,44 @@ +use axum::{ + extract::{Query, State}, + http::{HeaderMap, StatusCode}, + response::IntoResponse, + Extension, +}; + +use crate::{platform::http::request_id::RequestId, runtime::state::AppState}; + +use super::{ + api_key_service_error, ApiKeyExportQuery, ClientApiKeyExportData, ClientApiKeyExportEntry, +}; +use crate::admin::api::{ + account_export_ids, require_admin_session, AdminEnvelope, AdminError, AdminResponse, +}; + +pub async fn export_api_keys( + State(state): State<AppState>, + Extension(request_id): Extension<RequestId>, + headers: HeaderMap, + Query(query): Query<ApiKeyExportQuery>, +) -> Result<impl IntoResponse, AdminError> { + let request_id = request_id.as_str().to_string(); + require_admin_session(&state, &headers, &request_id).await?; + let ids = account_export_ids(query.ids.as_deref()); + let keys = match state.services.api_keys.export(ids).await { + Ok(keys) => keys, + Err(error) => return Err(api_key_service_error(error, request_id)), + }; + + // 安全边界:本地 cpr_ key 只导出可展示元数据,绝不导出 plaintext、key_hash 或 pepper。 + let data = ClientApiKeyExportData { + source_format: "rustLocalClientApiKeys", + rotation_required: true, + api_keys: keys + .into_iter() + .map(ClientApiKeyExportEntry::from) + .collect(), + }; + Ok(AdminResponse::new( + StatusCode::OK, + AdminEnvelope::ok(data, request_id), + )) +} diff --git a/src/admin/api/client_keys/import.rs b/src/admin/api/client_keys/import.rs new file mode 100644 index 000000000..04081b56e --- /dev/null +++ b/src/admin/api/client_keys/import.rs @@ -0,0 +1,40 @@ +use axum::{ + extract::State, http::HeaderMap, http::StatusCode, response::IntoResponse, Extension, Json, +}; +use serde_json::Value; + +use crate::{platform::http::request_id::RequestId, runtime::state::AppState}; + +use super::{api_key_service_error, ClientApiKeyImportData, ImportedClientApiKeyData}; +use crate::admin::api::{require_admin_session, AdminEnvelope, AdminError, AdminResponse}; + +pub async fn import_api_keys( + State(state): State<AppState>, + Extension(request_id): Extension<RequestId>, + headers: HeaderMap, + Json(payload): Json<Value>, +) -> Result<impl IntoResponse, AdminError> { + let request_id = request_id.as_str().to_string(); + require_admin_session(&state, &headers, &request_id).await?; + let imported = match state.services.api_keys.import(&payload).await { + Ok(imported) => imported, + Err(error) => return Err(api_key_service_error(error, request_id)), + }; + + Ok(AdminResponse::new( + StatusCode::OK, + AdminEnvelope::ok( + ClientApiKeyImportData { + imported: imported.imported, + skipped: imported.skipped, + rotated: true, + keys: imported + .keys + .into_iter() + .map(ImportedClientApiKeyData::new) + .collect(), + }, + request_id, + ), + )) +} diff --git a/src/admin/api/client_keys/lifecycle.rs b/src/admin/api/client_keys/lifecycle.rs new file mode 100644 index 000000000..d4f8cfa09 --- /dev/null +++ b/src/admin/api/client_keys/lifecycle.rs @@ -0,0 +1,116 @@ +use axum::{ + extract::{Path, State}, + http::{HeaderMap, StatusCode}, + response::IntoResponse, + Extension, Json, +}; + +use crate::{platform::http::request_id::RequestId, runtime::state::AppState}; + +use super::{ + api_key_not_found, api_key_service_error, BatchDeleteClientApiKeysData, + BatchDeleteClientApiKeysRequest, ClientApiKeyData, DeleteClientApiKeyData, + UpdateClientApiKeyLabelRequest, UpdateClientApiKeyStatusData, UpdateClientApiKeyStatusRequest, +}; +use crate::admin::api::{require_admin_session, AdminEnvelope, AdminError, AdminResponse}; + +pub async fn batch_delete_api_keys( + State(state): State<AppState>, + Extension(request_id): Extension<RequestId>, + headers: HeaderMap, + Json(payload): Json<BatchDeleteClientApiKeysRequest>, +) -> Result<impl IntoResponse, AdminError> { + let request_id = request_id.as_str().to_string(); + require_admin_session(&state, &headers, &request_id).await?; + let deleted = match state.services.api_keys.batch_delete(payload.ids).await { + Ok(deleted) => deleted, + Err(error) => return Err(api_key_service_error(error, request_id)), + }; + + Ok(AdminResponse::new( + StatusCode::OK, + AdminEnvelope::ok( + BatchDeleteClientApiKeysData { + deleted: deleted.deleted, + not_found: deleted.not_found, + }, + request_id, + ), + )) +} + +pub async fn update_api_key_label( + State(state): State<AppState>, + Extension(request_id): Extension<RequestId>, + headers: HeaderMap, + Path(key_id): Path<String>, + Json(payload): Json<UpdateClientApiKeyLabelRequest>, +) -> Result<impl IntoResponse, AdminError> { + let request_id = request_id.as_str().to_string(); + require_admin_session(&state, &headers, &request_id).await?; + + // label 是后台显示备注,不能影响 cpr_ key 的 hash、prefix 或启用状态。 + match state + .services + .api_keys + .update_label(&key_id, payload.label) + .await + { + Ok(Some(key)) => Ok(AdminResponse::new( + StatusCode::OK, + AdminEnvelope::ok(ClientApiKeyData::from(key), request_id), + )), + Ok(None) => Err(api_key_not_found(request_id)), + Err(error) => Err(api_key_service_error(error, request_id)), + } +} + +pub async fn update_api_key_status( + State(state): State<AppState>, + Extension(request_id): Extension<RequestId>, + headers: HeaderMap, + Path(key_id): Path<String>, + Json(payload): Json<UpdateClientApiKeyStatusRequest>, +) -> Result<impl IntoResponse, AdminError> { + let request_id = request_id.as_str().to_string(); + require_admin_session(&state, &headers, &request_id).await?; + + match state + .services + .api_keys + .update_status(key_id, &payload.status) + .await + { + Ok(Some(updated)) => Ok(AdminResponse::new( + StatusCode::OK, + AdminEnvelope::ok( + UpdateClientApiKeyStatusData { + id: updated.id, + enabled: updated.enabled, + }, + request_id, + ), + )), + Ok(None) => Err(api_key_not_found(request_id)), + Err(error) => Err(api_key_service_error(error, request_id)), + } +} + +pub async fn delete_api_key( + State(state): State<AppState>, + Extension(request_id): Extension<RequestId>, + headers: HeaderMap, + Path(key_id): Path<String>, +) -> Result<impl IntoResponse, AdminError> { + let request_id = request_id.as_str().to_string(); + require_admin_session(&state, &headers, &request_id).await?; + + match state.services.api_keys.delete(&key_id).await { + Ok(true) => Ok(AdminResponse::new( + StatusCode::OK, + AdminEnvelope::ok(DeleteClientApiKeyData { deleted: true }, request_id), + )), + Ok(false) => Err(api_key_not_found(request_id)), + Err(error) => Err(api_key_service_error(error, request_id)), + } +} diff --git a/src/admin/api/client_keys/list.rs b/src/admin/api/client_keys/list.rs new file mode 100644 index 000000000..de34d86b6 --- /dev/null +++ b/src/admin/api/client_keys/list.rs @@ -0,0 +1,40 @@ +use axum::{ + extract::{Query, State}, + http::{HeaderMap, StatusCode}, + response::IntoResponse, + Extension, +}; + +use crate::{ + platform::http::request_id::RequestId, + runtime::state::AppState, + utils::pagination::{clamp_limit, Page}, +}; + +use super::{api_key_service_error, ApiKeysQuery, ClientApiKeyData}; +use crate::admin::api::{require_admin_session, AdminError, AdminPageEnvelope, AdminResponse}; + +pub async fn api_keys( + State(state): State<AppState>, + Extension(request_id): Extension<RequestId>, + headers: HeaderMap, + Query(query): Query<ApiKeysQuery>, +) -> Result<impl IntoResponse, AdminError> { + let request_id = request_id.as_str().to_string(); + require_admin_session(&state, &headers, &request_id).await?; + let limit = clamp_limit(query.limit.unwrap_or(50)); + match state.services.api_keys.list(query.cursor, limit).await { + Ok(page) => { + let Page { items, next_cursor } = page; + let page = Page { + items: items.into_iter().map(ClientApiKeyData::from).collect(), + next_cursor, + }; + Ok(AdminResponse::new( + StatusCode::OK, + AdminPageEnvelope::ok(page, limit, request_id), + )) + } + Err(error) => Err(api_key_service_error(error, request_id)), + } +} diff --git a/src/admin/api/client_keys/mod.rs b/src/admin/api/client_keys/mod.rs new file mode 100644 index 000000000..012424ecc --- /dev/null +++ b/src/admin/api/client_keys/mod.rs @@ -0,0 +1,316 @@ +use axum::http::StatusCode; +use serde::{Deserialize, Serialize}; + +use crate::{ + admin::client_keys::service::{ApiKeyServiceError, ImportedClientApiKey}, + platform::identity::client_key_repository::StoredClientApiKey, +}; + +use super::AdminError; + +pub mod create; +pub mod export; +pub mod import; +pub mod lifecycle; +pub mod list; + +pub use create::create_api_key; +pub use export::export_api_keys; +pub use import::import_api_keys; +pub use lifecycle::{ + batch_delete_api_keys, delete_api_key, update_api_key_label, update_api_key_status, +}; +pub use list::api_keys; + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ApiKeysQuery { + pub cursor: Option<String>, + pub limit: Option<u32>, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ApiKeyExportQuery { + pub ids: Option<String>, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CreateApiKeyRequest { + pub name: String, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ClientApiKeyData { + pub id: String, + pub name: String, + pub label: Option<String>, + pub prefix: String, + pub enabled: bool, + pub created_at: String, + pub last_used_at: Option<String>, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CreatedClientApiKeyData { + pub id: String, + pub name: String, + pub label: Option<String>, + pub prefix: String, + pub enabled: bool, + pub created_at: String, + pub last_used_at: Option<String>, + pub plaintext: String, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct UpdateClientApiKeyStatusRequest { + pub status: String, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct UpdateClientApiKeyLabelRequest { + pub label: Option<String>, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct UpdateClientApiKeyStatusData { + pub id: String, + pub enabled: bool, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BatchDeleteClientApiKeysRequest { + pub ids: Vec<String>, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct BatchDeleteClientApiKeysData { + pub deleted: u32, + pub not_found: Vec<String>, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DeleteClientApiKeyData { + pub deleted: bool, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ClientApiKeyExportData { + pub source_format: &'static str, + pub rotation_required: bool, + pub api_keys: Vec<ClientApiKeyExportEntry>, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ClientApiKeyExportEntry { + pub id: String, + pub name: String, + pub label: Option<String>, + pub prefix: String, + pub enabled: bool, + pub created_at: String, + pub last_used_at: Option<String>, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ClientApiKeyImportData { + pub imported: u32, + pub skipped: u32, + pub rotated: bool, + pub keys: Vec<ImportedClientApiKeyData>, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ImportedClientApiKeyData { + pub source_id: Option<String>, + pub source_prefix: Option<String>, + pub id: String, + pub name: String, + pub label: Option<String>, + pub prefix: String, + pub enabled: bool, + pub created_at: String, + pub last_used_at: Option<String>, + pub plaintext: String, +} + +impl From<StoredClientApiKey> for ClientApiKeyData { + fn from(key: StoredClientApiKey) -> Self { + Self { + id: key.id, + name: key.name, + label: key.label, + prefix: key.prefix, + enabled: key.enabled, + created_at: key.created_at, + last_used_at: key.last_used_at, + } + } +} + +impl From<StoredClientApiKey> for ClientApiKeyExportEntry { + fn from(key: StoredClientApiKey) -> Self { + Self { + id: key.id, + name: key.name, + label: key.label, + prefix: key.prefix, + enabled: key.enabled, + created_at: key.created_at, + last_used_at: key.last_used_at, + } + } +} + +impl CreatedClientApiKeyData { + pub(super) fn new(key: StoredClientApiKey, plaintext: String) -> Self { + Self { + id: key.id, + name: key.name, + label: key.label, + prefix: key.prefix, + enabled: key.enabled, + created_at: key.created_at, + last_used_at: key.last_used_at, + plaintext, + } + } +} + +impl ImportedClientApiKeyData { + pub(super) fn new(imported: ImportedClientApiKey) -> Self { + let ImportedClientApiKey { + key, + plaintext, + source_id, + source_prefix, + } = imported; + Self { + source_id, + source_prefix, + id: key.id, + name: key.name, + label: key.label, + prefix: key.prefix, + enabled: key.enabled, + created_at: key.created_at, + last_used_at: key.last_used_at, + plaintext, + } + } +} + +pub(super) fn api_key_service_error(error: ApiKeyServiceError, request_id: String) -> AdminError { + match error { + ApiKeyServiceError::RepositoryUnavailable => AdminError::new( + StatusCode::INTERNAL_SERVER_ERROR, + 50001, + "API key repository is not initialized", + request_id, + ), + ApiKeyServiceError::HasherUnavailable => AdminError::new( + StatusCode::INTERNAL_SERVER_ERROR, + 50001, + "API key hasher is not initialized", + request_id, + ), + ApiKeyServiceError::List => AdminError::new( + StatusCode::INTERNAL_SERVER_ERROR, + 50001, + "Failed to list API keys", + request_id, + ), + ApiKeyServiceError::Export => AdminError::new( + StatusCode::INTERNAL_SERVER_ERROR, + 50001, + "Failed to export API keys", + request_id, + ), + ApiKeyServiceError::Import => AdminError::new( + StatusCode::INTERNAL_SERVER_ERROR, + 50001, + "Failed to import API key", + request_id, + ), + ApiKeyServiceError::Create => AdminError::new( + StatusCode::INTERNAL_SERVER_ERROR, + 50001, + "Failed to create API key", + request_id, + ), + ApiKeyServiceError::Delete => AdminError::new( + StatusCode::INTERNAL_SERVER_ERROR, + 50001, + "Failed to delete API key", + request_id, + ), + ApiKeyServiceError::UpdateLabel => AdminError::new( + StatusCode::INTERNAL_SERVER_ERROR, + 50001, + "Failed to update API key label", + request_id, + ), + ApiKeyServiceError::UpdateStatus => AdminError::new( + StatusCode::INTERNAL_SERVER_ERROR, + 50001, + "Failed to update API key status", + request_id, + ), + ApiKeyServiceError::Verify => AdminError::new( + StatusCode::INTERNAL_SERVER_ERROR, + 50001, + "Failed to verify API key", + request_id, + ), + ApiKeyServiceError::InvalidStatus(message) => { + AdminError::new(StatusCode::BAD_REQUEST, 40001, message, request_id) + } + ApiKeyServiceError::EmptyName => AdminError::new( + StatusCode::BAD_REQUEST, + 40001, + "API key name is required", + request_id, + ), + ApiKeyServiceError::EmptyIds => AdminError::new( + StatusCode::BAD_REQUEST, + 40001, + "API key ids are required", + request_id, + ), + ApiKeyServiceError::LabelTooLong => AdminError::new( + StatusCode::BAD_REQUEST, + 40001, + "API key label must be 64 characters or fewer", + request_id, + ), + ApiKeyServiceError::NoImportableKeys => AdminError::new( + StatusCode::BAD_REQUEST, + 40001, + "No importable API keys found", + request_id, + ), + } +} + +pub(super) fn api_key_not_found(request_id: String) -> AdminError { + AdminError::new( + StatusCode::NOT_FOUND, + 40401, + "API key not found", + request_id, + ) +} diff --git a/src/admin/http/diagnostics.rs b/src/admin/api/diagnostics.rs similarity index 95% rename from src/admin/http/diagnostics.rs rename to src/admin/api/diagnostics.rs index 6f08f34ea..89343bd63 100644 --- a/src/admin/http/diagnostics.rs +++ b/src/admin/api/diagnostics.rs @@ -6,7 +6,7 @@ use axum::{ }; use crate::{ - codex::serving::http::diagnostics::diagnostics_data, platform::http::middleware::RequestId, + codex::serving::http::diagnostics::diagnostics_data, platform::http::request_id::RequestId, runtime::state::AppState, }; diff --git a/src/admin/api/logs/detail.rs b/src/admin/api/logs/detail.rs new file mode 100644 index 000000000..b41ba4a2c --- /dev/null +++ b/src/admin/api/logs/detail.rs @@ -0,0 +1,35 @@ +use axum::{ + extract::{Path, State}, + http::{HeaderMap, StatusCode}, + response::IntoResponse, + Extension, +}; + +use crate::{platform::http::request_id::RequestId, runtime::state::AppState}; + +use super::log_service_error; +use crate::admin::api::{require_admin_session, AdminEnvelope, AdminError, AdminResponse}; + +pub async fn log_detail( + State(state): State<AppState>, + Extension(request_id): Extension<RequestId>, + headers: HeaderMap, + Path(log_id): Path<String>, +) -> Result<impl IntoResponse, AdminError> { + let request_id = request_id.as_str().to_string(); + require_admin_session(&state, &headers, &request_id).await?; + + match state.services.logs.get(&log_id).await { + Ok(Some(log)) => Ok(AdminResponse::new( + StatusCode::OK, + AdminEnvelope::ok(log, request_id), + )), + Ok(None) => Err(AdminError::new( + StatusCode::NOT_FOUND, + 40401, + "Log event not found", + request_id, + )), + Err(error) => Err(log_service_error(error, request_id)), + } +} diff --git a/src/admin/api/logs/mod.rs b/src/admin/api/logs/mod.rs new file mode 100644 index 000000000..399699eb8 --- /dev/null +++ b/src/admin/api/logs/mod.rs @@ -0,0 +1,60 @@ +use axum::http::StatusCode; + +use crate::codex::events::service::LogServiceError; + +use super::AdminError; + +pub mod detail; +pub mod query; +pub mod state; + +pub use detail::log_detail; +pub use query::logs; +pub use state::{clear_logs, logs_state, update_logs_state}; + +pub(super) fn log_service_error(error: LogServiceError, request_id: String) -> AdminError { + match error { + LogServiceError::RepositoryUnavailable => AdminError::new( + StatusCode::INTERNAL_SERVER_ERROR, + 50001, + "Event log repository is not initialized", + request_id, + ), + LogServiceError::List => AdminError::new( + StatusCode::INTERNAL_SERVER_ERROR, + 50001, + "Failed to list event logs", + request_id, + ), + LogServiceError::Get => AdminError::new( + StatusCode::INTERNAL_SERVER_ERROR, + 50001, + "Failed to load event log", + request_id, + ), + LogServiceError::Count => AdminError::new( + StatusCode::INTERNAL_SERVER_ERROR, + 50001, + "Failed to count event logs", + request_id, + ), + LogServiceError::Clear => AdminError::new( + StatusCode::INTERNAL_SERVER_ERROR, + 50001, + "Failed to clear event logs", + request_id, + ), + LogServiceError::Write => AdminError::new( + StatusCode::INTERNAL_SERVER_ERROR, + 50001, + "Failed to write event log", + request_id, + ), + LogServiceError::InvalidCapacity => AdminError::new( + StatusCode::UNPROCESSABLE_ENTITY, + 42201, + "logsCapacity must be greater than 0", + request_id, + ), + } +} diff --git a/src/admin/api/logs/query.rs b/src/admin/api/logs/query.rs new file mode 100644 index 000000000..6ba3cb07c --- /dev/null +++ b/src/admin/api/logs/query.rs @@ -0,0 +1,71 @@ +use axum::{ + extract::{Query, State}, + http::{HeaderMap, StatusCode}, + response::IntoResponse, + Extension, +}; +use serde::Deserialize; + +use crate::{ + codex::events::service::LogListFilter, platform::http::request_id::RequestId, + runtime::state::AppState, utils::pagination::clamp_limit, +}; + +use super::log_service_error; +use crate::admin::api::{require_admin_session, AdminError, AdminPageEnvelope, AdminResponse}; + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct LogsQuery { + pub cursor: Option<String>, + pub limit: Option<u32>, + pub kind: Option<String>, + pub level: Option<String>, + pub request_id: Option<String>, + pub account_id: Option<String>, + pub route: Option<String>, + pub model: Option<String>, + pub status_code: Option<i64>, + pub search: Option<String>, +} + +pub async fn logs( + State(state): State<AppState>, + Extension(request_id): Extension<RequestId>, + headers: HeaderMap, + Query(query): Query<LogsQuery>, +) -> Result<impl IntoResponse, AdminError> { + let request_id = request_id.as_str().to_string(); + require_admin_session(&state, &headers, &request_id).await?; + + let limit = clamp_limit(query.limit.unwrap_or(50)); + let cursor = query.cursor.clone(); + match state.services.logs.list(cursor, limit, query.into()).await { + Ok(page) => Ok(AdminResponse::new( + StatusCode::OK, + AdminPageEnvelope::ok(page, limit, request_id), + )), + Err(error) => Err(log_service_error(error, request_id)), + } +} + +impl From<LogsQuery> for LogListFilter { + fn from(query: LogsQuery) -> Self { + Self { + kind: non_empty(query.kind), + level: non_empty(query.level), + request_id: non_empty(query.request_id), + account_id: non_empty(query.account_id), + route: non_empty(query.route), + model: non_empty(query.model), + status_code: query.status_code, + search: non_empty(query.search), + } + } +} + +fn non_empty(value: Option<String>) -> Option<String> { + value + .map(|value| value.trim().to_string()) + .filter(|value| !value.is_empty()) +} diff --git a/src/admin/api/logs/state.rs b/src/admin/api/logs/state.rs new file mode 100644 index 000000000..0d7d13105 --- /dev/null +++ b/src/admin/api/logs/state.rs @@ -0,0 +1,120 @@ +use axum::{ + extract::State, + http::{HeaderMap, StatusCode}, + response::IntoResponse, + Extension, Json, +}; +use serde::{Deserialize, Serialize}; + +use crate::{ + codex::events::service::{ClearLogs, LogState, LogStateUpdate}, + platform::http::request_id::RequestId, + runtime::state::AppState, +}; + +use super::log_service_error; +use crate::admin::api::{require_admin_session, AdminEnvelope, AdminError, AdminResponse}; + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct LogStateData { + pub enabled: bool, + pub capacity: u32, + pub capture_body: bool, + pub stored_count: u64, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ClearLogsData { + pub cleared: u64, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct UpdateLogStateRequest { + pub enabled: Option<bool>, + pub capacity: Option<u32>, + pub capture_body: Option<bool>, +} + +pub async fn logs_state( + State(state): State<AppState>, + Extension(request_id): Extension<RequestId>, + headers: HeaderMap, +) -> Result<impl IntoResponse, AdminError> { + let request_id = request_id.as_str().to_string(); + require_admin_session(&state, &headers, &request_id).await?; + + match state.services.logs.state().await { + Ok(state) => Ok(AdminResponse::new( + StatusCode::OK, + AdminEnvelope::ok(LogStateData::from(state), request_id), + )), + Err(error) => Err(log_service_error(error, request_id)), + } +} + +pub async fn update_logs_state( + State(state): State<AppState>, + Extension(request_id): Extension<RequestId>, + headers: HeaderMap, + Json(payload): Json<UpdateLogStateRequest>, +) -> Result<impl IntoResponse, AdminError> { + let request_id = request_id.as_str().to_string(); + require_admin_session(&state, &headers, &request_id).await?; + + match state.services.logs.update_state(payload.into()).await { + Ok(state) => Ok(AdminResponse::new( + StatusCode::OK, + AdminEnvelope::ok(LogStateData::from(state), request_id), + )), + Err(error) => Err(log_service_error(error, request_id)), + } +} + +pub async fn clear_logs( + State(state): State<AppState>, + Extension(request_id): Extension<RequestId>, + headers: HeaderMap, +) -> Result<impl IntoResponse, AdminError> { + let request_id = request_id.as_str().to_string(); + require_admin_session(&state, &headers, &request_id).await?; + + match state.services.logs.clear().await { + Ok(cleared) => Ok(AdminResponse::new( + StatusCode::OK, + AdminEnvelope::ok(ClearLogsData::from(cleared), request_id), + )), + Err(error) => Err(log_service_error(error, request_id)), + } +} + +impl From<LogState> for LogStateData { + fn from(state: LogState) -> Self { + Self { + enabled: state.enabled, + capacity: state.capacity, + capture_body: state.capture_body, + stored_count: state.stored_count, + } + } +} + +impl From<ClearLogs> for ClearLogsData { + fn from(cleared: ClearLogs) -> Self { + Self { + cleared: cleared.cleared, + } + } +} + +impl From<UpdateLogStateRequest> for LogStateUpdate { + fn from(request: UpdateLogStateRequest) -> Self { + Self { + enabled: request.enabled, + capacity: request.capacity, + capture_body: request.capture_body, + } + } +} diff --git a/src/admin/http/mod.rs b/src/admin/api/mod.rs similarity index 81% rename from src/admin/http/mod.rs rename to src/admin/api/mod.rs index 557bf39a6..be0f22d6b 100644 --- a/src/admin/http/mod.rs +++ b/src/admin/api/mod.rs @@ -1,17 +1,17 @@ pub mod accounts; -pub mod api_keys; -pub mod auth; +pub mod client_keys; pub mod diagnostics; pub mod logs; pub mod models; pub mod response; pub mod router; +pub mod session; pub mod settings; pub mod usage; use self::{ accounts::{account_export_ids, account_status_value}, - auth::require_admin_session, + session::require_admin_session, }; pub use self::{ diff --git a/src/admin/http/models.rs b/src/admin/api/models.rs similarity index 96% rename from src/admin/http/models.rs rename to src/admin/api/models.rs index a5350129b..33b33c88d 100644 --- a/src/admin/http/models.rs +++ b/src/admin/api/models.rs @@ -7,8 +7,8 @@ use axum::{ use serde::Serialize; use crate::{ - codex::accounts::models::service::{ModelRefreshResult, ModelServiceError}, - platform::http::middleware::RequestId, + codex::models::service::{ModelRefreshResult, ModelServiceError}, + platform::http::request_id::RequestId, runtime::state::AppState, }; diff --git a/src/admin/http/response.rs b/src/admin/api/response.rs similarity index 100% rename from src/admin/http/response.rs rename to src/admin/api/response.rs diff --git a/src/admin/api/router.rs b/src/admin/api/router.rs new file mode 100644 index 000000000..e055623f2 --- /dev/null +++ b/src/admin/api/router.rs @@ -0,0 +1,111 @@ +use axum::{ + routing::{delete, get, patch, post}, + Router, +}; + +use crate::runtime::state::AppState; + +use super::{ + accounts::{ + account_quota, accounts, auth_callback, auth_code_relay, auth_device_login, + auth_device_poll, auth_login_start, batch_delete_accounts, batch_update_account_status, + create_account, delete_account, delete_account_cookies, export_accounts, + get_account_cookies, health_check_accounts, import_accounts, import_cli_auth, + quota_warnings, refresh_account, reset_account_usage, set_account_cookies, + update_account_label, update_account_status, + }, + client_keys::{ + api_keys, batch_delete_api_keys, create_api_key, delete_api_key, export_api_keys, + import_api_keys, update_api_key_label, update_api_key_status, + }, + diagnostics::diagnostics, + logs::{clear_logs, log_detail, logs, logs_state, update_logs_state}, + models::refresh_models, + session::{auth_logout, auth_status, login}, + settings::{settings, update_settings}, + usage::{usage_stats, usage_stats_summary}, +}; + +pub fn router() -> Router<AppState> { + Router::new() + .route("/api/admin/login", post(login)) + .route("/api/admin/auth/status", get(auth_status)) + .route("/api/admin/auth/logout", post(auth_logout)) + .route("/api/admin/auth/login-start", post(auth_login_start)) + .route("/api/admin/auth/code-relay", post(auth_code_relay)) + .route("/api/admin/auth/callback", get(auth_callback)) + .route("/auth/openai/callback", get(auth_callback)) + .route("/api/admin/auth/device-login", post(auth_device_login)) + .route( + "/api/admin/auth/device-poll/{device_code}", + get(auth_device_poll), + ) + .route("/api/admin/diagnostics", get(diagnostics)) + .route("/api/admin/logs", get(logs).delete(clear_logs)) + .route( + "/api/admin/logs/state", + get(logs_state).patch(update_logs_state), + ) + .route("/api/admin/logs/{log_id}", get(log_detail)) + .route("/api/admin/settings", get(settings).patch(update_settings)) + .route("/api/admin/refresh-models", post(refresh_models)) + .route("/api/admin/usage-stats", get(usage_stats)) + .route("/api/admin/usage-stats/summary", get(usage_stats_summary)) + .route("/api/admin/accounts", get(accounts).post(create_account)) + .route( + "/api/admin/accounts/health-check", + post(health_check_accounts), + ) + .route("/api/admin/accounts/quota-warnings", get(quota_warnings)) + .route( + "/api/admin/accounts/batch-delete", + post(batch_delete_accounts), + ) + .route( + "/api/admin/accounts/batch-status", + post(batch_update_account_status), + ) + .route("/api/admin/accounts/export", get(export_accounts)) + .route( + "/api/admin/accounts/{account_id}/refresh", + post(refresh_account), + ) + .route( + "/api/admin/accounts/{account_id}/reset-usage", + post(reset_account_usage), + ) + .route("/api/admin/accounts/{account_id}/quota", get(account_quota)) + .route( + "/api/admin/accounts/{account_id}/cookies", + get(get_account_cookies) + .post(set_account_cookies) + .delete(delete_account_cookies), + ) + .route("/api/admin/accounts/{account_id}", delete(delete_account)) + .route( + "/api/admin/accounts/{account_id}/label", + patch(update_account_label), + ) + .route( + "/api/admin/accounts/{account_id}/status", + patch(update_account_status), + ) + .route("/api/admin/accounts/import", post(import_accounts)) + .route("/api/admin/accounts/import-cli", post(import_cli_auth)) + .route("/api/admin/api-keys", get(api_keys).post(create_api_key)) + .route("/api/admin/api-keys/export", get(export_api_keys)) + .route("/api/admin/api-keys/import", post(import_api_keys)) + .route( + "/api/admin/api-keys/batch-delete", + post(batch_delete_api_keys), + ) + .route("/api/admin/api-keys/{key_id}", delete(delete_api_key)) + .route( + "/api/admin/api-keys/{key_id}/label", + patch(update_api_key_label), + ) + .route( + "/api/admin/api-keys/{key_id}/status", + patch(update_api_key_status), + ) +} diff --git a/src/admin/http/auth.rs b/src/admin/api/session.rs similarity index 99% rename from src/admin/http/auth.rs rename to src/admin/api/session.rs index f39711173..a2b1d27be 100644 --- a/src/admin/http/auth.rs +++ b/src/admin/api/session.rs @@ -10,12 +10,12 @@ use axum::{ use serde::{Deserialize, Serialize}; use crate::{ - admin::auth::service::{ + admin::session::service::{ AdminAuthPoolSummary as ServiceAdminAuthPoolSummary, AdminAuthServiceError, AdminAuthStatus as ServiceAdminAuthStatus, AdminAuthUser as ServiceAdminAuthUser, AdminSessionValidationError, }, - platform::http::{auth::admin_session_id, middleware::RequestId}, + platform::http::{auth::admin_session_id, request_id::RequestId}, runtime::state::AppState, }; diff --git a/src/admin/http/settings.rs b/src/admin/api/settings.rs similarity index 99% rename from src/admin/http/settings.rs rename to src/admin/api/settings.rs index 9e0d6032a..487700cf1 100644 --- a/src/admin/http/settings.rs +++ b/src/admin/api/settings.rs @@ -11,7 +11,7 @@ use serde_json::Value; use crate::{ admin::settings::{SettingsServiceError, SettingsUpdate}, config::{AppConfig, QuotaWarningThresholds}, - platform::http::middleware::RequestId, + platform::http::request_id::RequestId, runtime::state::AppState, }; diff --git a/src/admin/http/usage.rs b/src/admin/api/usage.rs similarity index 99% rename from src/admin/http/usage.rs rename to src/admin/api/usage.rs index 46b85e827..ada03ef37 100644 --- a/src/admin/http/usage.rs +++ b/src/admin/api/usage.rs @@ -11,7 +11,7 @@ use crate::{ AccountRepositoryError, AccountUsageListRecord, AccountUsageSummary, }, codex::usage::service::UsageServiceError, - platform::http::middleware::RequestId, + platform::http::request_id::RequestId, runtime::state::AppState, utils::pagination::{clamp_limit, Page}, }; diff --git a/src/admin/client_keys/mod.rs b/src/admin/client_keys/mod.rs new file mode 100644 index 000000000..1f278a4d5 --- /dev/null +++ b/src/admin/client_keys/mod.rs @@ -0,0 +1 @@ +pub mod service; diff --git a/src/admin/auth/api_key.rs b/src/admin/client_keys/service.rs similarity index 98% rename from src/admin/auth/api_key.rs rename to src/admin/client_keys/service.rs index 3875c630c..688b473fb 100644 --- a/src/admin/auth/api_key.rs +++ b/src/admin/client_keys/service.rs @@ -2,8 +2,8 @@ use serde_json::Value; use crate::{ platform::identity::{ - api_key::ApiKeyHasher, - api_key_repository::{ClientApiKeyRepository, StoredClientApiKey}, + client_key::ApiKeyHasher, + client_key_repository::{ClientApiKeyRepository, StoredClientApiKey}, }, utils::{json::first_string, pagination::Page}, }; diff --git a/src/admin/http/api_keys.rs b/src/admin/http/api_keys.rs deleted file mode 100644 index 007b06dc3..000000000 --- a/src/admin/http/api_keys.rs +++ /dev/null @@ -1,519 +0,0 @@ -use axum::{ - extract::{Path, Query, State}, - http::{HeaderMap, StatusCode}, - response::IntoResponse, - Extension, Json, -}; -use serde::{Deserialize, Serialize}; -use serde_json::Value; - -use crate::{ - admin::auth::api_key::{ApiKeyServiceError, ImportedClientApiKey}, - platform::{http::middleware::RequestId, identity::api_key_repository::StoredClientApiKey}, - runtime::state::AppState, - utils::pagination::{clamp_limit, Page}, -}; - -use super::{ - account_export_ids, require_admin_session, AdminEnvelope, AdminError, AdminPageEnvelope, - AdminResponse, -}; - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ApiKeysQuery { - pub cursor: Option<String>, - pub limit: Option<u32>, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ApiKeyExportQuery { - pub ids: Option<String>, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CreateApiKeyRequest { - pub name: String, -} - -#[derive(Debug, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct ClientApiKeyData { - pub id: String, - pub name: String, - pub label: Option<String>, - pub prefix: String, - pub enabled: bool, - pub created_at: String, - pub last_used_at: Option<String>, -} - -#[derive(Debug, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct CreatedClientApiKeyData { - pub id: String, - pub name: String, - pub label: Option<String>, - pub prefix: String, - pub enabled: bool, - pub created_at: String, - pub last_used_at: Option<String>, - pub plaintext: String, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct UpdateClientApiKeyStatusRequest { - pub status: String, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct UpdateClientApiKeyLabelRequest { - pub label: Option<String>, -} - -#[derive(Debug, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct UpdateClientApiKeyStatusData { - pub id: String, - pub enabled: bool, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct BatchDeleteClientApiKeysRequest { - pub ids: Vec<String>, -} - -#[derive(Debug, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct BatchDeleteClientApiKeysData { - pub deleted: u32, - pub not_found: Vec<String>, -} - -#[derive(Debug, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct DeleteClientApiKeyData { - pub deleted: bool, -} - -#[derive(Debug, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct ClientApiKeyExportData { - pub source_format: &'static str, - pub rotation_required: bool, - pub api_keys: Vec<ClientApiKeyExportEntry>, -} - -#[derive(Debug, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct ClientApiKeyExportEntry { - pub id: String, - pub name: String, - pub label: Option<String>, - pub prefix: String, - pub enabled: bool, - pub created_at: String, - pub last_used_at: Option<String>, -} - -#[derive(Debug, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct ClientApiKeyImportData { - pub imported: u32, - pub skipped: u32, - pub rotated: bool, - pub keys: Vec<ImportedClientApiKeyData>, -} - -#[derive(Debug, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct ImportedClientApiKeyData { - pub source_id: Option<String>, - pub source_prefix: Option<String>, - pub id: String, - pub name: String, - pub label: Option<String>, - pub prefix: String, - pub enabled: bool, - pub created_at: String, - pub last_used_at: Option<String>, - pub plaintext: String, -} - -impl From<StoredClientApiKey> for ClientApiKeyData { - fn from(key: StoredClientApiKey) -> Self { - Self { - id: key.id, - name: key.name, - label: key.label, - prefix: key.prefix, - enabled: key.enabled, - created_at: key.created_at, - last_used_at: key.last_used_at, - } - } -} - -impl From<StoredClientApiKey> for ClientApiKeyExportEntry { - fn from(key: StoredClientApiKey) -> Self { - Self { - id: key.id, - name: key.name, - label: key.label, - prefix: key.prefix, - enabled: key.enabled, - created_at: key.created_at, - last_used_at: key.last_used_at, - } - } -} - -impl CreatedClientApiKeyData { - fn new(key: StoredClientApiKey, plaintext: String) -> Self { - Self { - id: key.id, - name: key.name, - label: key.label, - prefix: key.prefix, - enabled: key.enabled, - created_at: key.created_at, - last_used_at: key.last_used_at, - plaintext, - } - } -} - -impl ImportedClientApiKeyData { - fn new(imported: ImportedClientApiKey) -> Self { - let ImportedClientApiKey { - key, - plaintext, - source_id, - source_prefix, - } = imported; - Self { - source_id, - source_prefix, - id: key.id, - name: key.name, - label: key.label, - prefix: key.prefix, - enabled: key.enabled, - created_at: key.created_at, - last_used_at: key.last_used_at, - plaintext, - } - } -} - -fn api_key_service_error(error: ApiKeyServiceError, request_id: String) -> AdminError { - match error { - ApiKeyServiceError::RepositoryUnavailable => AdminError::new( - StatusCode::INTERNAL_SERVER_ERROR, - 50001, - "API key repository is not initialized", - request_id, - ), - ApiKeyServiceError::HasherUnavailable => AdminError::new( - StatusCode::INTERNAL_SERVER_ERROR, - 50001, - "API key hasher is not initialized", - request_id, - ), - ApiKeyServiceError::List => AdminError::new( - StatusCode::INTERNAL_SERVER_ERROR, - 50001, - "Failed to list API keys", - request_id, - ), - ApiKeyServiceError::Export => AdminError::new( - StatusCode::INTERNAL_SERVER_ERROR, - 50001, - "Failed to export API keys", - request_id, - ), - ApiKeyServiceError::Import => AdminError::new( - StatusCode::INTERNAL_SERVER_ERROR, - 50001, - "Failed to import API key", - request_id, - ), - ApiKeyServiceError::Create => AdminError::new( - StatusCode::INTERNAL_SERVER_ERROR, - 50001, - "Failed to create API key", - request_id, - ), - ApiKeyServiceError::Delete => AdminError::new( - StatusCode::INTERNAL_SERVER_ERROR, - 50001, - "Failed to delete API key", - request_id, - ), - ApiKeyServiceError::UpdateLabel => AdminError::new( - StatusCode::INTERNAL_SERVER_ERROR, - 50001, - "Failed to update API key label", - request_id, - ), - ApiKeyServiceError::UpdateStatus => AdminError::new( - StatusCode::INTERNAL_SERVER_ERROR, - 50001, - "Failed to update API key status", - request_id, - ), - ApiKeyServiceError::Verify => AdminError::new( - StatusCode::INTERNAL_SERVER_ERROR, - 50001, - "Failed to verify API key", - request_id, - ), - ApiKeyServiceError::InvalidStatus(message) => { - AdminError::new(StatusCode::BAD_REQUEST, 40001, message, request_id) - } - ApiKeyServiceError::EmptyName => AdminError::new( - StatusCode::BAD_REQUEST, - 40001, - "API key name is required", - request_id, - ), - ApiKeyServiceError::EmptyIds => AdminError::new( - StatusCode::BAD_REQUEST, - 40001, - "API key ids are required", - request_id, - ), - ApiKeyServiceError::LabelTooLong => AdminError::new( - StatusCode::BAD_REQUEST, - 40001, - "API key label must be 64 characters or fewer", - request_id, - ), - ApiKeyServiceError::NoImportableKeys => AdminError::new( - StatusCode::BAD_REQUEST, - 40001, - "No importable API keys found", - request_id, - ), - } -} - -fn api_key_not_found(request_id: String) -> AdminError { - AdminError::new( - StatusCode::NOT_FOUND, - 40401, - "API key not found", - request_id, - ) -} - -pub async fn api_keys( - State(state): State<AppState>, - Extension(request_id): Extension<RequestId>, - headers: HeaderMap, - Query(query): Query<ApiKeysQuery>, -) -> Result<impl IntoResponse, AdminError> { - let request_id = request_id.as_str().to_string(); - require_admin_session(&state, &headers, &request_id).await?; - let limit = clamp_limit(query.limit.unwrap_or(50)); - match state.services.api_keys.list(query.cursor, limit).await { - Ok(page) => { - let Page { items, next_cursor } = page; - let page = Page { - items: items.into_iter().map(ClientApiKeyData::from).collect(), - next_cursor, - }; - Ok(AdminResponse::new( - StatusCode::OK, - AdminPageEnvelope::ok(page, limit, request_id), - )) - } - Err(error) => Err(api_key_service_error(error, request_id)), - } -} - -pub async fn export_api_keys( - State(state): State<AppState>, - Extension(request_id): Extension<RequestId>, - headers: HeaderMap, - Query(query): Query<ApiKeyExportQuery>, -) -> Result<impl IntoResponse, AdminError> { - let request_id = request_id.as_str().to_string(); - require_admin_session(&state, &headers, &request_id).await?; - let ids = account_export_ids(query.ids.as_deref()); - let keys = match state.services.api_keys.export(ids).await { - Ok(keys) => keys, - Err(error) => return Err(api_key_service_error(error, request_id)), - }; - - // 安全边界:本地 cpr_ key 只导出可展示元数据,绝不导出 plaintext、key_hash 或 pepper。 - let data = ClientApiKeyExportData { - source_format: "rustLocalClientApiKeys", - rotation_required: true, - api_keys: keys - .into_iter() - .map(ClientApiKeyExportEntry::from) - .collect(), - }; - Ok(AdminResponse::new( - StatusCode::OK, - AdminEnvelope::ok(data, request_id), - )) -} - -pub async fn import_api_keys( - State(state): State<AppState>, - Extension(request_id): Extension<RequestId>, - headers: HeaderMap, - Json(payload): Json<Value>, -) -> Result<impl IntoResponse, AdminError> { - let request_id = request_id.as_str().to_string(); - require_admin_session(&state, &headers, &request_id).await?; - let imported = match state.services.api_keys.import(&payload).await { - Ok(imported) => imported, - Err(error) => return Err(api_key_service_error(error, request_id)), - }; - - Ok(AdminResponse::new( - StatusCode::OK, - AdminEnvelope::ok( - ClientApiKeyImportData { - imported: imported.imported, - skipped: imported.skipped, - rotated: true, - keys: imported - .keys - .into_iter() - .map(ImportedClientApiKeyData::new) - .collect(), - }, - request_id, - ), - )) -} - -pub async fn create_api_key( - State(state): State<AppState>, - Extension(request_id): Extension<RequestId>, - headers: HeaderMap, - Json(payload): Json<CreateApiKeyRequest>, -) -> Result<impl IntoResponse, AdminError> { - let request_id = request_id.as_str().to_string(); - require_admin_session(&state, &headers, &request_id).await?; - match state.services.api_keys.create(&payload.name).await { - Ok(created) => Ok(AdminResponse::new( - StatusCode::OK, - AdminEnvelope::ok( - CreatedClientApiKeyData::new(created.key, created.plaintext), - request_id, - ), - )), - Err(error) => Err(api_key_service_error(error, request_id)), - } -} - -pub async fn batch_delete_api_keys( - State(state): State<AppState>, - Extension(request_id): Extension<RequestId>, - headers: HeaderMap, - Json(payload): Json<BatchDeleteClientApiKeysRequest>, -) -> Result<impl IntoResponse, AdminError> { - let request_id = request_id.as_str().to_string(); - require_admin_session(&state, &headers, &request_id).await?; - let deleted = match state.services.api_keys.batch_delete(payload.ids).await { - Ok(deleted) => deleted, - Err(error) => return Err(api_key_service_error(error, request_id)), - }; - - Ok(AdminResponse::new( - StatusCode::OK, - AdminEnvelope::ok( - BatchDeleteClientApiKeysData { - deleted: deleted.deleted, - not_found: deleted.not_found, - }, - request_id, - ), - )) -} - -pub async fn update_api_key_label( - State(state): State<AppState>, - Extension(request_id): Extension<RequestId>, - headers: HeaderMap, - Path(key_id): Path<String>, - Json(payload): Json<UpdateClientApiKeyLabelRequest>, -) -> Result<impl IntoResponse, AdminError> { - let request_id = request_id.as_str().to_string(); - require_admin_session(&state, &headers, &request_id).await?; - - // label 是后台显示备注,不能影响 cpr_ key 的 hash、prefix 或启用状态。 - match state - .services - .api_keys - .update_label(&key_id, payload.label) - .await - { - Ok(Some(key)) => Ok(AdminResponse::new( - StatusCode::OK, - AdminEnvelope::ok(ClientApiKeyData::from(key), request_id), - )), - Ok(None) => Err(api_key_not_found(request_id)), - Err(error) => Err(api_key_service_error(error, request_id)), - } -} - -pub async fn update_api_key_status( - State(state): State<AppState>, - Extension(request_id): Extension<RequestId>, - headers: HeaderMap, - Path(key_id): Path<String>, - Json(payload): Json<UpdateClientApiKeyStatusRequest>, -) -> Result<impl IntoResponse, AdminError> { - let request_id = request_id.as_str().to_string(); - require_admin_session(&state, &headers, &request_id).await?; - - match state - .services - .api_keys - .update_status(key_id, &payload.status) - .await - { - Ok(Some(updated)) => Ok(AdminResponse::new( - StatusCode::OK, - AdminEnvelope::ok( - UpdateClientApiKeyStatusData { - id: updated.id, - enabled: updated.enabled, - }, - request_id, - ), - )), - Ok(None) => Err(api_key_not_found(request_id)), - Err(error) => Err(api_key_service_error(error, request_id)), - } -} - -pub async fn delete_api_key( - State(state): State<AppState>, - Extension(request_id): Extension<RequestId>, - headers: HeaderMap, - Path(key_id): Path<String>, -) -> Result<impl IntoResponse, AdminError> { - let request_id = request_id.as_str().to_string(); - require_admin_session(&state, &headers, &request_id).await?; - - match state.services.api_keys.delete(&key_id).await { - Ok(true) => Ok(AdminResponse::new( - StatusCode::OK, - AdminEnvelope::ok(DeleteClientApiKeyData { deleted: true }, request_id), - )), - Ok(false) => Err(api_key_not_found(request_id)), - Err(error) => Err(api_key_service_error(error, request_id)), - } -} diff --git a/src/admin/http/logs.rs b/src/admin/http/logs.rs deleted file mode 100644 index fda072f77..000000000 --- a/src/admin/http/logs.rs +++ /dev/null @@ -1,247 +0,0 @@ -use axum::{ - extract::{Path, Query, State}, - http::{HeaderMap, StatusCode}, - response::IntoResponse, - Extension, Json, -}; -use serde::{Deserialize, Serialize}; - -use crate::{ - codex::logs::service::{ClearLogs, LogListFilter, LogServiceError, LogState, LogStateUpdate}, - platform::http::middleware::RequestId, - runtime::state::AppState, - utils::pagination::clamp_limit, -}; - -use super::{require_admin_session, AdminEnvelope, AdminError, AdminPageEnvelope, AdminResponse}; - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct LogsQuery { - pub cursor: Option<String>, - pub limit: Option<u32>, - pub kind: Option<String>, - pub level: Option<String>, - pub request_id: Option<String>, - pub account_id: Option<String>, - pub route: Option<String>, - pub model: Option<String>, - pub status_code: Option<i64>, - pub search: Option<String>, -} - -#[derive(Debug, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct LogStateData { - pub enabled: bool, - pub capacity: u32, - pub capture_body: bool, - pub stored_count: u64, -} - -#[derive(Debug, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct ClearLogsData { - pub cleared: u64, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct UpdateLogStateRequest { - pub enabled: Option<bool>, - pub capacity: Option<u32>, - pub capture_body: Option<bool>, -} - -pub async fn logs( - State(state): State<AppState>, - Extension(request_id): Extension<RequestId>, - headers: HeaderMap, - Query(query): Query<LogsQuery>, -) -> Result<impl IntoResponse, AdminError> { - let request_id = request_id.as_str().to_string(); - require_admin_session(&state, &headers, &request_id).await?; - - let limit = clamp_limit(query.limit.unwrap_or(50)); - let cursor = query.cursor.clone(); - match state.services.logs.list(cursor, limit, query.into()).await { - Ok(page) => Ok(AdminResponse::new( - StatusCode::OK, - AdminPageEnvelope::ok(page, limit, request_id), - )), - Err(error) => Err(log_service_error(error, request_id)), - } -} - -pub async fn logs_state( - State(state): State<AppState>, - Extension(request_id): Extension<RequestId>, - headers: HeaderMap, -) -> Result<impl IntoResponse, AdminError> { - let request_id = request_id.as_str().to_string(); - require_admin_session(&state, &headers, &request_id).await?; - - match state.services.logs.state().await { - Ok(state) => Ok(AdminResponse::new( - StatusCode::OK, - AdminEnvelope::ok(LogStateData::from(state), request_id), - )), - Err(error) => Err(log_service_error(error, request_id)), - } -} - -pub async fn update_logs_state( - State(state): State<AppState>, - Extension(request_id): Extension<RequestId>, - headers: HeaderMap, - Json(payload): Json<UpdateLogStateRequest>, -) -> Result<impl IntoResponse, AdminError> { - let request_id = request_id.as_str().to_string(); - require_admin_session(&state, &headers, &request_id).await?; - - match state.services.logs.update_state(payload.into()).await { - Ok(state) => Ok(AdminResponse::new( - StatusCode::OK, - AdminEnvelope::ok(LogStateData::from(state), request_id), - )), - Err(error) => Err(log_service_error(error, request_id)), - } -} - -pub async fn log_detail( - State(state): State<AppState>, - Extension(request_id): Extension<RequestId>, - headers: HeaderMap, - Path(log_id): Path<String>, -) -> Result<impl IntoResponse, AdminError> { - let request_id = request_id.as_str().to_string(); - require_admin_session(&state, &headers, &request_id).await?; - - match state.services.logs.get(&log_id).await { - Ok(Some(log)) => Ok(AdminResponse::new( - StatusCode::OK, - AdminEnvelope::ok(log, request_id), - )), - Ok(None) => Err(AdminError::new( - StatusCode::NOT_FOUND, - 40401, - "Log event not found", - request_id, - )), - Err(error) => Err(log_service_error(error, request_id)), - } -} - -pub async fn clear_logs( - State(state): State<AppState>, - Extension(request_id): Extension<RequestId>, - headers: HeaderMap, -) -> Result<impl IntoResponse, AdminError> { - let request_id = request_id.as_str().to_string(); - require_admin_session(&state, &headers, &request_id).await?; - - match state.services.logs.clear().await { - Ok(cleared) => Ok(AdminResponse::new( - StatusCode::OK, - AdminEnvelope::ok(ClearLogsData::from(cleared), request_id), - )), - Err(error) => Err(log_service_error(error, request_id)), - } -} - -impl From<LogState> for LogStateData { - fn from(state: LogState) -> Self { - Self { - enabled: state.enabled, - capacity: state.capacity, - capture_body: state.capture_body, - stored_count: state.stored_count, - } - } -} - -impl From<ClearLogs> for ClearLogsData { - fn from(cleared: ClearLogs) -> Self { - Self { - cleared: cleared.cleared, - } - } -} - -impl From<UpdateLogStateRequest> for LogStateUpdate { - fn from(request: UpdateLogStateRequest) -> Self { - Self { - enabled: request.enabled, - capacity: request.capacity, - capture_body: request.capture_body, - } - } -} - -impl From<LogsQuery> for LogListFilter { - fn from(query: LogsQuery) -> Self { - Self { - kind: non_empty(query.kind), - level: non_empty(query.level), - request_id: non_empty(query.request_id), - account_id: non_empty(query.account_id), - route: non_empty(query.route), - model: non_empty(query.model), - status_code: query.status_code, - search: non_empty(query.search), - } - } -} - -fn non_empty(value: Option<String>) -> Option<String> { - value - .map(|value| value.trim().to_string()) - .filter(|value| !value.is_empty()) -} - -fn log_service_error(error: LogServiceError, request_id: String) -> AdminError { - match error { - LogServiceError::RepositoryUnavailable => AdminError::new( - StatusCode::INTERNAL_SERVER_ERROR, - 50001, - "Event log repository is not initialized", - request_id, - ), - LogServiceError::List => AdminError::new( - StatusCode::INTERNAL_SERVER_ERROR, - 50001, - "Failed to list event logs", - request_id, - ), - LogServiceError::Get => AdminError::new( - StatusCode::INTERNAL_SERVER_ERROR, - 50001, - "Failed to load event log", - request_id, - ), - LogServiceError::Count => AdminError::new( - StatusCode::INTERNAL_SERVER_ERROR, - 50001, - "Failed to count event logs", - request_id, - ), - LogServiceError::Clear => AdminError::new( - StatusCode::INTERNAL_SERVER_ERROR, - 50001, - "Failed to clear event logs", - request_id, - ), - LogServiceError::Write => AdminError::new( - StatusCode::INTERNAL_SERVER_ERROR, - 50001, - "Failed to write event log", - request_id, - ), - LogServiceError::InvalidCapacity => AdminError::new( - StatusCode::UNPROCESSABLE_ENTITY, - 42201, - "logsCapacity must be greater than 0", - request_id, - ), - } -} diff --git a/src/admin/http/router.rs b/src/admin/http/router.rs deleted file mode 100644 index 33d3a6486..000000000 --- a/src/admin/http/router.rs +++ /dev/null @@ -1,103 +0,0 @@ -use axum::{ - routing::{delete, get, patch, post}, - Router, -}; - -use crate::runtime::state::AppState; - -use super::{ - accounts::{ - account_quota, accounts, auth_callback, auth_code_relay, auth_device_login, - auth_device_poll, auth_login_start, batch_delete_accounts, batch_update_account_status, - create_account, delete_account, delete_account_cookies, export_accounts, - get_account_cookies, health_check_accounts, import_accounts, import_cli_auth, - quota_warnings, refresh_account, reset_account_usage, set_account_cookies, - update_account_label, update_account_status, - }, - api_keys::{ - api_keys, batch_delete_api_keys, create_api_key, delete_api_key, export_api_keys, - import_api_keys, update_api_key_label, update_api_key_status, - }, - auth::{auth_logout, auth_status, login}, - diagnostics::diagnostics, - logs::{clear_logs, log_detail, logs, logs_state, update_logs_state}, - models::refresh_models, - settings::{settings, update_settings}, - usage::{usage_stats, usage_stats_summary}, -}; - -pub fn router() -> Router<AppState> { - Router::new() - .route("/admin/login", post(login)) - .route("/admin/auth/status", get(auth_status)) - .route("/admin/auth/logout", post(auth_logout)) - .route("/admin/auth/login-start", post(auth_login_start)) - .route("/admin/auth/code-relay", post(auth_code_relay)) - .route("/admin/auth/callback", get(auth_callback)) - // OpenAI OAuth 客户端注册的是 /auth/callback;handler 内仍要求管理员会话。 - .route("/auth/callback", get(auth_callback)) - .route("/admin/auth/device-login", post(auth_device_login)) - .route( - "/admin/auth/device-poll/{device_code}", - get(auth_device_poll), - ) - .route("/admin/diagnostics", get(diagnostics)) - .route("/admin/logs", get(logs).delete(clear_logs)) - .route( - "/admin/logs/state", - get(logs_state).patch(update_logs_state), - ) - .route("/admin/logs/{log_id}", get(log_detail)) - .route("/admin/settings", get(settings).patch(update_settings)) - .route("/admin/refresh-models", post(refresh_models)) - .route("/admin/usage-stats", get(usage_stats)) - .route("/admin/usage-stats/summary", get(usage_stats_summary)) - .route("/admin/accounts", get(accounts).post(create_account)) - .route("/admin/accounts/health-check", post(health_check_accounts)) - .route("/admin/accounts/quota-warnings", get(quota_warnings)) - .route("/admin/accounts/batch-delete", post(batch_delete_accounts)) - .route( - "/admin/accounts/batch-status", - post(batch_update_account_status), - ) - .route("/admin/accounts/export", get(export_accounts)) - .route( - "/admin/accounts/{account_id}/refresh", - post(refresh_account), - ) - .route( - "/admin/accounts/{account_id}/reset-usage", - post(reset_account_usage), - ) - .route("/admin/accounts/{account_id}/quota", get(account_quota)) - .route( - "/admin/accounts/{account_id}/cookies", - get(get_account_cookies) - .post(set_account_cookies) - .delete(delete_account_cookies), - ) - .route("/admin/accounts/{account_id}", delete(delete_account)) - .route( - "/admin/accounts/{account_id}/label", - patch(update_account_label), - ) - .route( - "/admin/accounts/{account_id}/status", - patch(update_account_status), - ) - .route("/admin/accounts/import", post(import_accounts)) - .route("/admin/accounts/import-cli", post(import_cli_auth)) - .route("/admin/api-keys", get(api_keys).post(create_api_key)) - .route("/admin/api-keys/export", get(export_api_keys)) - .route("/admin/api-keys/import", post(import_api_keys)) - .route("/admin/api-keys/batch-delete", post(batch_delete_api_keys)) - .route("/admin/api-keys/{key_id}", delete(delete_api_key)) - .route( - "/admin/api-keys/{key_id}/label", - patch(update_api_key_label), - ) - .route( - "/admin/api-keys/{key_id}/status", - patch(update_api_key_status), - ) -} diff --git a/src/admin/mod.rs b/src/admin/mod.rs index bb1b296b8..1c84f2fc2 100644 --- a/src/admin/mod.rs +++ b/src/admin/mod.rs @@ -1,4 +1,5 @@ -pub mod auth; -pub mod http; +pub mod api; +pub mod client_keys; +pub mod session; pub mod settings; pub mod tasks; diff --git a/src/admin/auth/mod.rs b/src/admin/session/mod.rs similarity index 68% rename from src/admin/auth/mod.rs rename to src/admin/session/mod.rs index b901e5d1b..5acc82448 100644 --- a/src/admin/auth/mod.rs +++ b/src/admin/session/mod.rs @@ -1,3 +1,2 @@ -pub mod api_key; pub mod repository; pub mod service; diff --git a/src/admin/auth/repository.rs b/src/admin/session/repository.rs similarity index 100% rename from src/admin/auth/repository.rs rename to src/admin/session/repository.rs diff --git a/src/admin/auth/service.rs b/src/admin/session/service.rs similarity index 99% rename from src/admin/auth/service.rs rename to src/admin/session/service.rs index 664cf9b6c..d6e3c1725 100644 --- a/src/admin/auth/service.rs +++ b/src/admin/session/service.rs @@ -5,7 +5,7 @@ use tokio::sync::Mutex; use uuid::Uuid; use crate::{ - admin::auth::repository::AdminAuthRepository, + admin::session::repository::AdminAuthRepository, codex::accounts::service::{AccountService, ValidatedAccountImportError}, codex::accounts::{ model::AccountStatus, diff --git a/src/admin/tasks/session_cleanup.rs b/src/admin/tasks/session_cleanup.rs index c362b8f04..e43ced5a2 100644 --- a/src/admin/tasks/session_cleanup.rs +++ b/src/admin/tasks/session_cleanup.rs @@ -2,7 +2,7 @@ use chrono::Utc; use tokio::time::{interval, Duration}; use tracing::{error, info}; -use crate::admin::auth::repository::AdminAuthRepository; +use crate::admin::session::repository::AdminAuthRepository; use crate::runtime::tasks::types::SchedulerHandle; /// 会话清理调度器 - 定期删除过期的管理员会话 diff --git a/src/codex/accounts/cf_path_block.rs b/src/codex/accounts/cloudflare_challenge.rs similarity index 100% rename from src/codex/accounts/cf_path_block.rs rename to src/codex/accounts/cloudflare_challenge.rs diff --git a/src/codex/accounts/mod.rs b/src/codex/accounts/mod.rs index 36f4f5ff0..8e6014fdb 100644 --- a/src/codex/accounts/mod.rs +++ b/src/codex/accounts/mod.rs @@ -1,9 +1,8 @@ -pub mod cf_path_block; +pub mod cloudflare_challenge; pub mod cookies; pub mod jwt; pub mod lifecycle; pub mod model; -pub mod models; pub mod pool; pub mod repository; pub mod service; diff --git a/src/codex/accounts/repository.rs b/src/codex/accounts/repository/accounts.rs similarity index 97% rename from src/codex/accounts/repository.rs rename to src/codex/accounts/repository/accounts.rs index ed9f8667f..c02be35b5 100644 --- a/src/codex/accounts/repository.rs +++ b/src/codex/accounts/repository/accounts.rs @@ -9,13 +9,6 @@ use crate::{ utils::pagination::{decode_cursor, encode_cursor, Page}, }; -mod lease; -mod quota; -mod token; -mod usage; - -pub use usage::AccountUsageRepository; - #[derive(Debug, Error)] pub enum AccountRepositoryError { #[error("database error: {0}")] @@ -155,8 +148,8 @@ pub struct AccountUsageSummary { #[derive(Clone)] pub struct AccountRepository { - pool: SqlitePool, - secret_box: SecretBox, + pub(super) pool: SqlitePool, + pub(super) secret_box: SecretBox, } const INSERT_ACCOUNT_SQL: &str = r" @@ -644,7 +637,7 @@ impl AccountRepository { } } -fn status_to_db(status: AccountStatus) -> &'static str { +pub(crate) fn status_to_db(status: AccountStatus) -> &'static str { match status { AccountStatus::Active => "active", AccountStatus::Expired => "expired", @@ -667,11 +660,13 @@ fn status_from_db(value: &str) -> AccountRepositoryResult<AccountStatus> { } } -fn parse_optional_rfc3339(value: Option<String>) -> AccountRepositoryResult<Option<DateTime<Utc>>> { +pub(crate) fn parse_optional_rfc3339( + value: Option<String>, +) -> AccountRepositoryResult<Option<DateTime<Utc>>> { value.as_deref().map(parse_rfc3339).transpose() } -fn parse_rfc3339(value: &str) -> AccountRepositoryResult<DateTime<Utc>> { +pub(crate) fn parse_rfc3339(value: &str) -> AccountRepositoryResult<DateTime<Utc>> { Ok(DateTime::parse_from_rfc3339(value)?.with_timezone(&Utc)) } @@ -679,7 +674,7 @@ fn nonnegative_i64_to_u64(value: i64) -> u64 { value.max(0) as u64 } -fn optional_positive_i64_to_u64(value: Option<i64>) -> Option<u64> { +pub(crate) fn optional_positive_i64_to_u64(value: Option<i64>) -> Option<u64> { value .and_then(|value| u64::try_from(value).ok()) .filter(|value| *value > 0) diff --git a/src/codex/accounts/repository/lease.rs b/src/codex/accounts/repository/leases.rs similarity index 100% rename from src/codex/accounts/repository/lease.rs rename to src/codex/accounts/repository/leases.rs diff --git a/src/codex/accounts/repository/mod.rs b/src/codex/accounts/repository/mod.rs new file mode 100644 index 000000000..4fb4a3374 --- /dev/null +++ b/src/codex/accounts/repository/mod.rs @@ -0,0 +1,9 @@ +mod accounts; +mod leases; +mod quotas; +mod tokens; +mod usage; + +pub use accounts::*; +pub(crate) use accounts::{optional_positive_i64_to_u64, parse_optional_rfc3339, status_to_db}; +pub use usage::AccountUsageRepository; diff --git a/src/codex/accounts/repository/quota.rs b/src/codex/accounts/repository/quotas.rs similarity index 100% rename from src/codex/accounts/repository/quota.rs rename to src/codex/accounts/repository/quotas.rs diff --git a/src/codex/accounts/repository/token.rs b/src/codex/accounts/repository/tokens.rs similarity index 97% rename from src/codex/accounts/repository/token.rs rename to src/codex/accounts/repository/tokens.rs index 60cf1a351..a284ca625 100644 --- a/src/codex/accounts/repository/token.rs +++ b/src/codex/accounts/repository/tokens.rs @@ -185,7 +185,7 @@ mod tests { ); let access_token = SecretString::new("access-secret".to_string().into()); let refresh_token = SecretString::new("refresh-secret".to_string().into()); - let expires_at = super::super::parse_rfc3339("2026-06-14T00:00:00Z").unwrap(); + let expires_at = super::super::accounts::parse_rfc3339("2026-06-14T00:00:00Z").unwrap(); let write = repository .prepare_token_write(&access_token, Some(&refresh_token), Some(expires_at)) @@ -198,6 +198,6 @@ mod tests { write.access_token_expires_at.as_deref(), Some("2026-06-14T00:00:00+00:00") ); - assert!(super::super::parse_rfc3339(&write.updated_at).is_ok()); + assert!(super::super::accounts::parse_rfc3339(&write.updated_at).is_ok()); } } diff --git a/src/codex/accounts/service/health.rs b/src/codex/accounts/service/health.rs index b30f7761d..18342948e 100644 --- a/src/codex/accounts/service/health.rs +++ b/src/codex/accounts/service/health.rs @@ -13,7 +13,7 @@ use crate::{ repository::{AccountRepository, StoredAccount}, }, codex::gateway::fingerprint::model::Fingerprint, - codex::gateway::transport::client::{ + codex::gateway::transport::http_client::{ build_reqwest_client, CodexBackendClient, CodexClientError, CodexRequestContext, }, }; diff --git a/src/codex/accounts/service/import.rs b/src/codex/accounts/service/import.rs index 65dfb5a3c..974bb869d 100644 --- a/src/codex/accounts/service/import.rs +++ b/src/codex/accounts/service/import.rs @@ -13,8 +13,8 @@ use crate::{ }; use super::{ - runtime_pool::pool_account_from_stored, AccountImportCounts, AccountImportEntry, - AccountService, StoreImportAccountError, ValidatedAccountImportError, + pool_sync::pool_account_from_stored, AccountImportCounts, AccountImportEntry, AccountService, + StoreImportAccountError, ValidatedAccountImportError, }; enum StoredImportAccount { diff --git a/src/codex/accounts/service/mutation.rs b/src/codex/accounts/service/lifecycle.rs similarity index 97% rename from src/codex/accounts/service/mutation.rs rename to src/codex/accounts/service/lifecycle.rs index 8f44815b6..332759550 100644 --- a/src/codex/accounts/service/mutation.rs +++ b/src/codex/accounts/service/lifecycle.rs @@ -1,8 +1,8 @@ use crate::codex::accounts::{model::AccountStatus, repository::AccountRepository}; use super::{ - runtime_pool::pool_account_from_stored, AccountService, AccountServiceError, - BatchDeleteAccounts, BatchUpdateAccountStatus, UpdateAccountStatus, + pool_sync::pool_account_from_stored, AccountService, AccountServiceError, BatchDeleteAccounts, + BatchUpdateAccountStatus, UpdateAccountStatus, }; impl AccountService { diff --git a/src/codex/accounts/service/mod.rs b/src/codex/accounts/service/mod.rs index a7cab1424..f33b9ee57 100644 --- a/src/codex/accounts/service/mod.rs +++ b/src/codex/accounts/service/mod.rs @@ -1,10 +1,10 @@ pub mod cookies; pub mod health; pub mod import; -pub mod mutation; +pub mod lifecycle; +pub mod pool_sync; pub mod quota; pub mod refresh; -pub mod runtime_pool; use std::sync::Arc; diff --git a/src/codex/accounts/service/runtime_pool.rs b/src/codex/accounts/service/pool_sync.rs similarity index 100% rename from src/codex/accounts/service/runtime_pool.rs rename to src/codex/accounts/service/pool_sync.rs diff --git a/src/codex/accounts/service/refresh.rs b/src/codex/accounts/service/refresh.rs index 898879217..5300cb138 100644 --- a/src/codex/accounts/service/refresh.rs +++ b/src/codex/accounts/service/refresh.rs @@ -13,7 +13,7 @@ use crate::{ }; use super::{ - health::skipped_probe_result, runtime_pool::pool_account_from_stored, AccountProbeOutcome, + health::skipped_probe_result, pool_sync::pool_account_from_stored, AccountProbeOutcome, AccountProbeResult, AccountService, RefreshAccountError, }; diff --git a/src/codex/logs/event.rs b/src/codex/events/event.rs similarity index 100% rename from src/codex/logs/event.rs rename to src/codex/events/event.rs diff --git a/src/codex/logs/mod.rs b/src/codex/events/mod.rs similarity index 74% rename from src/codex/logs/mod.rs rename to src/codex/events/mod.rs index 1b03bc428..46e84672a 100644 --- a/src/codex/logs/mod.rs +++ b/src/codex/events/mod.rs @@ -1,4 +1,3 @@ pub mod event; pub mod repository; -pub mod rotation; pub mod service; diff --git a/src/codex/logs/repository.rs b/src/codex/events/repository.rs similarity index 99% rename from src/codex/logs/repository.rs rename to src/codex/events/repository.rs index 22cb18916..3f6e97aff 100644 --- a/src/codex/logs/repository.rs +++ b/src/codex/events/repository.rs @@ -1,7 +1,7 @@ use sqlx::{QueryBuilder, Row, Sqlite, SqlitePool}; use crate::{ - codex::logs::event::{EventLevel, EventLog}, + codex::events::event::{EventLevel, EventLog}, utils::pagination::{clamp_limit, decode_cursor, encode_cursor, Page}, }; diff --git a/src/codex/logs/service.rs b/src/codex/events/service.rs similarity index 99% rename from src/codex/logs/service.rs rename to src/codex/events/service.rs index 298c2d89c..64feaf692 100644 --- a/src/codex/logs/service.rs +++ b/src/codex/events/service.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use tokio::sync::RwLock; use crate::{ - codex::logs::{ + codex::events::{ event::EventLog, repository::{EventLogFilters, EventLogRepository}, }, diff --git a/src/codex/gateway/identity.rs b/src/codex/gateway/conversation_identity.rs similarity index 100% rename from src/codex/gateway/identity.rs rename to src/codex/gateway/conversation_identity.rs diff --git a/src/codex/gateway/installation.rs b/src/codex/gateway/installation_id.rs similarity index 100% rename from src/codex/gateway/installation.rs rename to src/codex/gateway/installation_id.rs diff --git a/src/codex/gateway/mod.rs b/src/codex/gateway/mod.rs index 45eb89c41..e65e13dcc 100644 --- a/src/codex/gateway/mod.rs +++ b/src/codex/gateway/mod.rs @@ -1,6 +1,6 @@ +pub mod conversation_identity; pub mod fingerprint; -pub mod identity; -pub mod installation; +pub mod installation_id; pub mod oauth; pub mod protocol; pub mod transport; diff --git a/src/codex/gateway/oauth/client.rs b/src/codex/gateway/oauth/client.rs index eeb52b4b7..f8cec21a9 100644 --- a/src/codex/gateway/oauth/client.rs +++ b/src/codex/gateway/oauth/client.rs @@ -19,7 +19,7 @@ const OPENAI_OAUTH_AUTHORIZE_ENDPOINT: &str = "https://auth.openai.com/oauth/aut const OPENAI_OAUTH_DEVICE_CODE_ENDPOINT: &str = "https://auth.openai.com/oauth/device/code"; const OPENAI_OAUTH_TOKEN_ENDPOINT: &str = "https://auth.openai.com/oauth/token"; const OAUTH_CALLBACK_PORT: u16 = 1455; -const OAUTH_CALLBACK_PATH: &str = "/auth/callback"; +const OAUTH_CALLBACK_PATH: &str = "/auth/openai/callback"; #[derive(Debug, Clone)] pub struct OAuthConfig { diff --git a/src/codex/gateway/oauth/cli_import.rs b/src/codex/gateway/oauth/codex_cli.rs similarity index 100% rename from src/codex/gateway/oauth/cli_import.rs rename to src/codex/gateway/oauth/codex_cli.rs diff --git a/src/codex/gateway/oauth/mod.rs b/src/codex/gateway/oauth/mod.rs index 9f465635a..7d3d0ddab 100644 --- a/src/codex/gateway/oauth/mod.rs +++ b/src/codex/gateway/oauth/mod.rs @@ -1,9 +1,9 @@ -pub mod cli_import; pub mod client; +pub mod codex_cli; pub mod refresh; pub mod token; -pub use cli_import::*; pub use client::*; +pub use codex_cli::*; pub use refresh::*; pub use token::*; diff --git a/src/codex/gateway/transport/client.rs b/src/codex/gateway/transport/http_client.rs similarity index 99% rename from src/codex/gateway/transport/client.rs rename to src/codex/gateway/transport/http_client.rs index 4045bedc7..8206a7af0 100644 --- a/src/codex/gateway/transport/client.rs +++ b/src/codex/gateway/transport/http_client.rs @@ -11,13 +11,12 @@ use serde_json::{Map, Value}; use thiserror::Error; use crate::{ - codex::accounts::models::catalog::BackendModelEntry, codex::gateway::fingerprint::model::Fingerprint, codex::gateway::transport::{ headers::build_ordered_codex_headers, sse::SseError, types::CodexResponsesRequest, - usage::{extract_sse_usage, TokenUsage}, + usage_events::{extract_sse_usage, TokenUsage}, websocket::{ append_rate_limit_updates, create_response_via_websocket, create_response_via_websocket_stream, create_response_via_websocket_stream_with_pool, @@ -26,6 +25,7 @@ use crate::{ SharedRateLimitUpdates, WebSocketSupportError, }, }, + codex::models::catalog::BackendModelEntry, }; #[derive(Debug, Error)] diff --git a/src/codex/gateway/transport/mod.rs b/src/codex/gateway/transport/mod.rs index 463ea73f3..ad6143042 100644 --- a/src/codex/gateway/transport/mod.rs +++ b/src/codex/gateway/transport/mod.rs @@ -1,7 +1,7 @@ -pub mod client; pub mod headers; +pub mod http_client; pub mod rate_limits; pub mod sse; pub mod types; -pub mod usage; +pub mod usage_events; pub mod websocket; diff --git a/src/codex/gateway/transport/usage.rs b/src/codex/gateway/transport/usage_events.rs similarity index 100% rename from src/codex/gateway/transport/usage.rs rename to src/codex/gateway/transport/usage_events.rs diff --git a/src/codex/gateway/transport/websocket.rs b/src/codex/gateway/transport/websocket/mod.rs similarity index 100% rename from src/codex/gateway/transport/websocket.rs rename to src/codex/gateway/transport/websocket/mod.rs diff --git a/src/codex/mod.rs b/src/codex/mod.rs index 366851074..b222d7c10 100644 --- a/src/codex/mod.rs +++ b/src/codex/mod.rs @@ -1,6 +1,7 @@ pub mod accounts; +pub mod events; pub mod gateway; -pub mod logs; +pub mod models; pub mod serving; pub mod tasks; pub mod usage; diff --git a/src/codex/accounts/models/catalog.rs b/src/codex/models/catalog.rs similarity index 100% rename from src/codex/accounts/models/catalog.rs rename to src/codex/models/catalog.rs diff --git a/src/codex/accounts/models/mod.rs b/src/codex/models/mod.rs similarity index 100% rename from src/codex/accounts/models/mod.rs rename to src/codex/models/mod.rs diff --git a/src/codex/accounts/models/repository.rs b/src/codex/models/repository.rs similarity index 95% rename from src/codex/accounts/models/repository.rs rename to src/codex/models/repository.rs index 23de105ac..5ec49256b 100644 --- a/src/codex/accounts/models/repository.rs +++ b/src/codex/models/repository.rs @@ -2,7 +2,7 @@ use chrono::Utc; use sqlx::{Row, SqlitePool}; use thiserror::Error; -use crate::codex::accounts::models::catalog::{CodexModelInfo, ModelPlanSnapshot}; +use crate::codex::models::catalog::{CodexModelInfo, ModelPlanSnapshot}; #[derive(Debug, Error)] pub enum ModelSnapshotRepositoryError { diff --git a/src/codex/accounts/models/service.rs b/src/codex/models/service.rs similarity index 98% rename from src/codex/accounts/models/service.rs rename to src/codex/models/service.rs index e89d5b294..a9fc285b0 100644 --- a/src/codex/accounts/models/service.rs +++ b/src/codex/models/service.rs @@ -3,19 +3,19 @@ use std::sync::Arc; use tokio::sync::Mutex; use crate::{ - codex::accounts::models::{ - catalog::{ModelCatalog, ModelPlanSnapshot}, - repository::ModelSnapshotRepository, - }, codex::accounts::{ model::{Account, AccountStatus}, pool::AccountPool, repository::AccountRepository, }, codex::gateway::fingerprint::model::Fingerprint, - codex::gateway::transport::client::{ + codex::gateway::transport::http_client::{ build_reqwest_client, CodexBackendClient, CodexRequestContext, }, + codex::models::{ + catalog::{ModelCatalog, ModelPlanSnapshot}, + repository::ModelSnapshotRepository, + }, config::AppConfig, }; diff --git a/src/codex/serving/chat.rs b/src/codex/serving/chat.rs index d262730be..5cf5af2f8 100644 --- a/src/codex/serving/chat.rs +++ b/src/codex/serving/chat.rs @@ -12,15 +12,15 @@ use axum::{ use serde_json::json; use crate::{ - codex::accounts::models::catalog::ModelCatalog, - codex::accounts::models::service::ModelService, + codex::events::event::EventLevel, codex::gateway::protocol::{ codex_to_openai::{ chat_completion_from_codex_sse, chat_completion_stream_from_codex_sse, openai_error, }, openai_to_codex::{translate_chat_to_codex, ChatCompletionRequest}, }, - codex::logs::event::EventLevel, + codex::models::catalog::ModelCatalog, + codex::models::service::ModelService, codex::serving::dispatch::{ classify_upstream_account_retry, no_available_accounts_response, normalize_service_tier_for_upstream, CodexRequestLogContext, CodexUpstreamService, diff --git a/src/codex/serving/diagnostics.rs b/src/codex/serving/diagnostics.rs index 267ae78b0..bf274110d 100644 --- a/src/codex/serving/diagnostics.rs +++ b/src/codex/serving/diagnostics.rs @@ -6,7 +6,7 @@ use serde::Serialize; use crate::{ codex::gateway::{ fingerprint::model::Fingerprint, - transport::client::{ + transport::http_client::{ build_reqwest_client, CodexBackendClient, CodexClientError, CodexRequestContext, }, }, diff --git a/src/codex/serving/dispatch/refresh.rs b/src/codex/serving/dispatch/account_refresh.rs similarity index 98% rename from src/codex/serving/dispatch/refresh.rs rename to src/codex/serving/dispatch/account_refresh.rs index 0c654c20e..ed14b4d3e 100644 --- a/src/codex/serving/dispatch/refresh.rs +++ b/src/codex/serving/dispatch/account_refresh.rs @@ -7,9 +7,9 @@ use crate::{ model::{Account, AccountStatus}, repository::TokenUpdate, }, + codex::events::event::{EventLevel, EventLog}, codex::gateway::oauth::{RefreshFailure, TokenPair}, codex::gateway::transport::types::CodexResponsesRequest, - codex::logs::event::{EventLevel, EventLog}, }; use super::CodexUpstreamDependencies; diff --git a/src/codex/serving/dispatch/fallback.rs b/src/codex/serving/dispatch/fallback.rs index d4b22021b..4f8ea5d71 100644 --- a/src/codex/serving/dispatch/fallback.rs +++ b/src/codex/serving/dispatch/fallback.rs @@ -7,7 +7,7 @@ use crate::codex::{ model::{Account, AccountStatus}, pool::AccountAcquireRequest, }, - gateway::transport::{client::CodexClientError, rate_limits::cooldown_with_jitter}, + gateway::transport::{http_client::CodexClientError, rate_limits::cooldown_with_jitter}, }; use super::{usage::record_request_attempt, CodexUpstreamDependencies}; diff --git a/src/codex/serving/dispatch/mod.rs b/src/codex/serving/dispatch/mod.rs index fe06189ef..b66cc9731 100644 --- a/src/codex/serving/dispatch/mod.rs +++ b/src/codex/serving/dispatch/mod.rs @@ -1,11 +1,11 @@ +mod account_refresh; pub mod affinity; -mod audit; pub mod fallback; mod limits; -pub mod refresh; // 上游调度辅助命名为 routing,避免出现 dispatch::dispatch 的模块套娃。 pub mod routing; pub mod stream; +mod stream_audit; pub mod usage; use std::{sync::Arc, time::Instant}; @@ -31,26 +31,26 @@ use crate::{ pool::{AccountAcquireRequest, AccountPool}, repository::AccountRepository, }, + codex::events::{ + event::{EventLevel, EventLog}, + service::LogService, + }, + codex::gateway::conversation_identity::{build_conversation_identity, ensure_prompt_cache_key}, codex::gateway::fingerprint::model::Fingerprint, - codex::gateway::identity::{build_conversation_identity, ensure_prompt_cache_key}, - codex::gateway::installation::get_installation_id, + codex::gateway::installation_id::get_installation_id, codex::gateway::oauth::TokenRefresher, codex::gateway::protocol::codex_to_openai::openai_error, codex::gateway::transport::{ - client::{ + http_client::{ build_reqwest_client, CodexBackendClient, CodexBackendStream, CodexBackendWebSocketStream, CodexClientError, CodexRequestContext, }, types::CodexResponsesRequest, - usage::TokenUsage, + usage_events::TokenUsage, websocket::{ transport_for_request, CodexTransport, CodexWebSocketError, CodexWebSocketPool, }, }, - codex::logs::{ - event::{EventLevel, EventLog}, - service::LogService, - }, config::AppConfig, }; @@ -65,13 +65,13 @@ pub(crate) use self::{ }; use self::affinity::{compute_variant_hash, SessionAffinityMap, SessionAffinityRepository}; -use self::audit::{StreamAudit, WebSocketStreamAudit}; use self::fallback::{ apply_upstream_account_retry_with_deps, apply_upstream_retry_and_acquire_fallback_with_deps, }; +use self::stream_audit::{StreamAudit, WebSocketStreamAudit}; use self::{ + account_refresh::refresh_account_after_unauthorized, limits::apply_rate_limit_headers_with_deps, - refresh::refresh_account_after_unauthorized, routing::request_domain, stream::{completed_response_metadata, ensure_stream_metadata}, usage::{record_empty_response_with_deps, record_usage_with_deps}, @@ -200,7 +200,7 @@ impl CodexUpstreamService { request: &CodexResponsesRequest, account: &Account, request_id: &str, - ) -> Result<crate::codex::gateway::transport::client::CodexBackendResponse, CodexClientError> + ) -> Result<crate::codex::gateway::transport::http_client::CodexBackendResponse, CodexClientError> { send_codex_request_with_refresh_retry_deps(&self.deps, request, account, request_id).await } @@ -466,7 +466,7 @@ async fn send_codex_request_with_refresh_retry_deps( request: &CodexResponsesRequest, account: &Account, request_id: &str, -) -> Result<crate::codex::gateway::transport::client::CodexBackendResponse, CodexClientError> { +) -> Result<crate::codex::gateway::transport::http_client::CodexBackendResponse, CodexClientError> { let result = match send_codex_request(deps, request, account, request_id).await { Err(CodexClientError::Upstream { status, @@ -503,7 +503,7 @@ async fn send_codex_request( request: &CodexResponsesRequest, account: &Account, request_id: &str, -) -> Result<crate::codex::gateway::transport::client::CodexBackendResponse, CodexClientError> { +) -> Result<crate::codex::gateway::transport::http_client::CodexBackendResponse, CodexClientError> { let request_domain = request_domain(&deps.config.api.base_url); let cookie_header = match (deps.cookie_repository.as_ref(), request_domain.as_deref()) { (Some(repo), Some(domain)) => repo.cookie_header(&account.id, domain).await.ok().flatten(), diff --git a/src/codex/serving/dispatch/audit.rs b/src/codex/serving/dispatch/stream_audit.rs similarity index 99% rename from src/codex/serving/dispatch/audit.rs rename to src/codex/serving/dispatch/stream_audit.rs index 405b4cd90..46eee3b54 100644 --- a/src/codex/serving/dispatch/audit.rs +++ b/src/codex/serving/dispatch/stream_audit.rs @@ -8,11 +8,11 @@ use crate::{ codex::gateway::transport::types::CodexResponsesRequest, codex::{ accounts::pool::AccountPool, + events::event::EventLevel, gateway::transport::{ - usage::extract_sse_usage, + usage_events::extract_sse_usage, websocket::{append_rate_limit_updates, CodexWebSocketError, SharedRateLimitUpdates}, }, - logs::event::EventLevel, }, }; diff --git a/src/codex/serving/dispatch/usage.rs b/src/codex/serving/dispatch/usage.rs index 3cb970a78..7d2105c58 100644 --- a/src/codex/serving/dispatch/usage.rs +++ b/src/codex/serving/dispatch/usage.rs @@ -1,5 +1,5 @@ use crate::{ - codex::accounts::repository::UsageDelta, codex::gateway::transport::usage::TokenUsage, + codex::accounts::repository::UsageDelta, codex::gateway::transport::usage_events::TokenUsage, }; use super::CodexUpstreamDependencies; diff --git a/src/codex/serving/http/chat.rs b/src/codex/serving/http/chat.rs index a7a02fa98..0c29f4b9b 100644 --- a/src/codex/serving/http/chat.rs +++ b/src/codex/serving/http/chat.rs @@ -11,7 +11,7 @@ use crate::{ codex::gateway::protocol::{ codex_to_openai::openai_error, openai_to_codex::ChatCompletionRequest, }, - platform::http::middleware::RequestId, + platform::http::request_id::RequestId, runtime::state::AppState, }; diff --git a/src/codex/serving/http/diagnostics.rs b/src/codex/serving/http/diagnostics.rs index 65e785627..648a3845d 100644 --- a/src/codex/serving/http/diagnostics.rs +++ b/src/codex/serving/http/diagnostics.rs @@ -10,10 +10,10 @@ use serde::Serialize; use serde_json::json; use crate::{ - admin::auth::service::AdminAuthPoolSummary, + admin::session::service::AdminAuthPoolSummary, codex::{accounts::pool::AccountCapacitySummary, gateway::fingerprint::model::Fingerprint}, config::AppConfig, - platform::http::middleware::RequestId, + platform::http::request_id::RequestId, runtime::state::AppState, }; diff --git a/src/codex/serving/http/errors.rs b/src/codex/serving/http/errors.rs index 36d562d17..22216597a 100644 --- a/src/codex/serving/http/errors.rs +++ b/src/codex/serving/http/errors.rs @@ -3,7 +3,7 @@ use serde_json::Value; use crate::{ codex::gateway::protocol::codex_to_openai::openai_error, - codex::gateway::transport::client::CodexClientError, + codex::gateway::transport::http_client::CodexClientError, }; pub(crate) fn codex_client_error_response(error: CodexClientError) -> (StatusCode, Json<Value>) { diff --git a/src/codex/serving/http/models.rs b/src/codex/serving/http/models.rs index c159b9503..dfd0e6702 100644 --- a/src/codex/serving/http/models.rs +++ b/src/codex/serving/http/models.rs @@ -6,7 +6,7 @@ use axum::{ }; use serde_json::{json, Value}; -use crate::{codex::accounts::models::catalog::ModelCatalog, runtime::state::AppState}; +use crate::{codex::models::catalog::ModelCatalog, runtime::state::AppState}; use super::{ auth::authorize_client_api_key, diff --git a/src/codex/serving/http/responses.rs b/src/codex/serving/http/responses.rs index bfbfe2e6d..768122cf5 100644 --- a/src/codex/serving/http/responses.rs +++ b/src/codex/serving/http/responses.rs @@ -8,7 +8,7 @@ use axum::{ Extension, }; -use crate::{platform::http::middleware::RequestId, runtime::state::AppState}; +use crate::{platform::http::request_id::RequestId, runtime::state::AppState}; use super::{auth::authorize_client_api_key, errors::missing_client_api_key_response}; diff --git a/src/codex/serving/responses.rs b/src/codex/serving/responses.rs index 9fa77b941..d15687464 100644 --- a/src/codex/serving/responses.rs +++ b/src/codex/serving/responses.rs @@ -10,11 +10,11 @@ use serde::Deserialize; use serde_json::{json, Map, Value}; use crate::{ - codex::accounts::models::service::ModelService, + codex::events::event::EventLevel, codex::gateway::fingerprint::model::Fingerprint, codex::gateway::protocol::codex_to_openai::openai_error, codex::gateway::transport::types::CodexResponsesRequest, - codex::logs::event::EventLevel, + codex::models::service::ModelService, codex::serving::dispatch::{ affinity::SessionAffinityRepositoryResult, classify_upstream_account_retry, completed_response_json, no_available_accounts_response, diff --git a/src/codex/tasks/mod.rs b/src/codex/tasks/mod.rs index fef684100..376a0c7eb 100644 --- a/src/codex/tasks/mod.rs +++ b/src/codex/tasks/mod.rs @@ -1,3 +1,3 @@ -pub mod model; -pub mod quota; -pub mod refresh; +pub mod model_refresh; +pub mod quota_refresh; +pub mod token_refresh; diff --git a/src/codex/tasks/model.rs b/src/codex/tasks/model_refresh.rs similarity index 96% rename from src/codex/tasks/model.rs rename to src/codex/tasks/model_refresh.rs index 8d9ca6933..dfc92dfa8 100644 --- a/src/codex/tasks/model.rs +++ b/src/codex/tasks/model_refresh.rs @@ -3,9 +3,7 @@ use std::time::Duration; use tokio::time::{interval, sleep}; use tracing::{info, warn}; -use crate::{ - codex::accounts::models::service::ModelService, runtime::tasks::types::SchedulerHandle, -}; +use crate::{codex::models::service::ModelService, runtime::tasks::types::SchedulerHandle}; /// 模型刷新调度器 - 定期从 Codex 后端刷新模型列表 /// @@ -78,7 +76,7 @@ impl ModelRefresher { // 如果是 NoAccounts 错误,跳过重试直接进入定期刷新 let is_no_accounts = matches!( e, - crate::codex::accounts::models::service::ModelServiceError::NoAccounts + crate::codex::models::service::ModelServiceError::NoAccounts ); if is_no_accounts { diff --git a/src/codex/tasks/quota.rs b/src/codex/tasks/quota_refresh.rs similarity index 100% rename from src/codex/tasks/quota.rs rename to src/codex/tasks/quota_refresh.rs diff --git a/src/codex/tasks/refresh.rs b/src/codex/tasks/token_refresh.rs similarity index 100% rename from src/codex/tasks/refresh.rs rename to src/codex/tasks/token_refresh.rs diff --git a/src/lib.rs b/src/lib.rs index f74db7b47..c75032fff 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,7 @@ pub mod admin; pub mod codex; pub mod runtime; +pub mod web; pub mod config; pub mod platform; diff --git a/src/main.rs b/src/main.rs index 3915e1c4c..c93618e0d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,6 @@ use codex_proxy_rs::{ - codex::logs::rotation::{init_tracing, RotationConfig}, config::AppConfig, + platform::logging::rotation::{init_tracing, RotationConfig}, runtime::{bootstrap::build_state, build_router, tasks::start_background_tasks}, }; use tokio::signal; diff --git a/src/platform/crypto/mod.rs b/src/platform/crypto/mod.rs new file mode 100644 index 000000000..a02525874 --- /dev/null +++ b/src/platform/crypto/mod.rs @@ -0,0 +1,3 @@ +pub mod secret_box; + +pub use secret_box::{CryptoError, CryptoResult, SecretBox}; diff --git a/src/platform/crypto.rs b/src/platform/crypto/secret_box.rs similarity index 100% rename from src/platform/crypto.rs rename to src/platform/crypto/secret_box.rs diff --git a/src/platform/http/mod.rs b/src/platform/http/mod.rs index 6e5639cba..52c7290a8 100644 --- a/src/platform/http/mod.rs +++ b/src/platform/http/mod.rs @@ -1,3 +1,3 @@ pub mod auth; pub mod health; -pub mod middleware; +pub mod request_id; diff --git a/src/platform/http/middleware.rs b/src/platform/http/request_id.rs similarity index 100% rename from src/platform/http/middleware.rs rename to src/platform/http/request_id.rs diff --git a/src/platform/identity/api_key.rs b/src/platform/identity/client_key.rs similarity index 100% rename from src/platform/identity/api_key.rs rename to src/platform/identity/client_key.rs diff --git a/src/platform/identity/api_key_repository.rs b/src/platform/identity/client_key_repository.rs similarity index 98% rename from src/platform/identity/api_key_repository.rs rename to src/platform/identity/client_key_repository.rs index 4b59db8da..4082d396f 100644 --- a/src/platform/identity/api_key_repository.rs +++ b/src/platform/identity/client_key_repository.rs @@ -4,7 +4,7 @@ use thiserror::Error; use uuid::Uuid; use crate::{ - platform::identity::api_key::{ApiKeyHasher, GeneratedClientApiKey}, + platform::identity::client_key::{ApiKeyHasher, GeneratedClientApiKey}, utils::pagination::{decode_cursor, encode_cursor, Page}, }; diff --git a/src/platform/identity/mod.rs b/src/platform/identity/mod.rs index be4c5cd44..d230d844f 100644 --- a/src/platform/identity/mod.rs +++ b/src/platform/identity/mod.rs @@ -1,4 +1,4 @@ pub mod admin_session; -pub mod api_key; -pub mod api_key_repository; +pub mod client_key; +pub mod client_key_repository; pub mod error; diff --git a/src/platform/logging/mod.rs b/src/platform/logging/mod.rs new file mode 100644 index 000000000..a72110a3e --- /dev/null +++ b/src/platform/logging/mod.rs @@ -0,0 +1 @@ +pub mod rotation; diff --git a/src/codex/logs/rotation.rs b/src/platform/logging/rotation.rs similarity index 100% rename from src/codex/logs/rotation.rs rename to src/platform/logging/rotation.rs diff --git a/src/platform/mod.rs b/src/platform/mod.rs index bca20dbf0..12b2bb3ab 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -1,4 +1,5 @@ pub mod crypto; pub mod http; pub mod identity; +pub mod logging; pub mod storage; diff --git a/src/runtime/bootstrap.rs b/src/runtime/bootstrap.rs index dcf0db0a3..84dec3bbb 100644 --- a/src/runtime/bootstrap.rs +++ b/src/runtime/bootstrap.rs @@ -1,7 +1,7 @@ use thiserror::Error; use crate::{ - admin::auth::repository::AdminAuthRepository, + admin::session::repository::AdminAuthRepository, codex::accounts::repository::AccountRepositoryError, codex::gateway::fingerprint::{model::Fingerprint, repository::FingerprintRepository}, codex::gateway::oauth::OpenAiOAuthRefresher, @@ -9,7 +9,9 @@ use crate::{ config::AppConfig, platform::{ crypto::{CryptoError, SecretBox}, - identity::{admin_session::hash_admin_password, api_key::ApiKeyHasher, error::AuthError}, + identity::{ + admin_session::hash_admin_password, client_key::ApiKeyHasher, error::AuthError, + }, storage::db::connect_sqlite, }, }; diff --git a/src/runtime/router.rs b/src/runtime/router.rs index 889308f77..de8d5e002 100644 --- a/src/runtime/router.rs +++ b/src/runtime/router.rs @@ -13,14 +13,14 @@ use tower_http::{ use tracing::Span; use crate::{ - admin::http as admin_http, + admin::api as admin_api, codex::serving::http::{ diagnostics::{debug_fingerprint, debug_upstream, diagnostics}, router as serving_http, }, platform::http::{ health::health, - middleware::{attach_request_id, RequestId}, + request_id::{attach_request_id, RequestId}, }, }; @@ -55,7 +55,7 @@ pub fn build_router(state: AppState) -> Router { .route("/debug/fingerprint", get(debug_fingerprint)) .route("/debug/upstream", get(debug_upstream)) .merge(serving_http::router()) - .merge(admin_http::router()) + .merge(admin_api::router()) .with_state(state) .layer(http_trace_layer()) .layer(from_fn(attach_request_id)) diff --git a/src/runtime/state.rs b/src/runtime/state.rs index 307f6f775..61820a6d8 100644 --- a/src/runtime/state.rs +++ b/src/runtime/state.rs @@ -4,12 +4,11 @@ use sqlx::SqlitePool; use tokio::sync::Mutex; use crate::admin::{ - auth::{api_key::ApiKeyService, repository::AdminAuthRepository, service::AdminAuthService}, + client_keys::service::ApiKeyService, + session::{repository::AdminAuthRepository, service::AdminAuthService}, settings::SettingsService, }; use crate::codex::accounts::cookies::repository::CookieRepository; -use crate::codex::accounts::models::repository::ModelSnapshotRepository; -use crate::codex::accounts::models::service::ModelService; use crate::codex::accounts::{ pool::{AccountPool, AccountPoolOptions, RotationStrategy}, repository::{AccountRepository, AccountRepositoryResult, AccountUsageRepository}, @@ -17,6 +16,8 @@ use crate::codex::accounts::{ }; use crate::codex::gateway::fingerprint::model::Fingerprint; use crate::codex::gateway::oauth::{OAuthClient, PkceSessionStore, TokenRefresher}; +use crate::codex::models::repository::ModelSnapshotRepository; +use crate::codex::models::service::ModelService; use crate::codex::serving::dispatch::affinity::{ SessionAffinityRepository, SessionAffinityRepositoryResult, }; @@ -25,11 +26,11 @@ use crate::codex::serving::{ chat::ChatService, diagnostics::DiagnosticsService, responses::ResponsesService, }; use crate::codex::usage::service::UsageService; -use crate::codex::{logs::repository::EventLogRepository, logs::service::LogService}; +use crate::codex::{events::repository::EventLogRepository, events::service::LogService}; use crate::config::AppConfig; use crate::platform::crypto::SecretBox; use crate::platform::identity::{ - api_key::ApiKeyHasher, api_key_repository::ClientApiKeyRepository, + client_key::ApiKeyHasher, client_key_repository::ClientApiKeyRepository, }; #[derive(Clone)] diff --git a/src/runtime/tasks/coordinator.rs b/src/runtime/tasks/coordinator.rs index 15fca7fc7..8b8c570f1 100644 --- a/src/runtime/tasks/coordinator.rs +++ b/src/runtime/tasks/coordinator.rs @@ -2,13 +2,16 @@ use sqlx::SqlitePool; use crate::{ admin::{ - auth::repository::AdminAuthRepository, tasks::session_cleanup::SessionCleanupScheduler, + session::repository::AdminAuthRepository, tasks::session_cleanup::SessionCleanupScheduler, }, codex::{ gateway::fingerprint::{ model::Fingerprint, repository::FingerprintRepository, update_checker::UpdateChecker, }, - tasks::{model::ModelRefresher, quota::QuotaRefresher, refresh::RefreshScheduler}, + tasks::{ + model_refresh::ModelRefresher, quota_refresh::QuotaRefresher, + token_refresh::RefreshScheduler, + }, }, config::AppConfig, runtime::state::AppState, diff --git a/src/web/assets.rs b/src/web/assets.rs new file mode 100644 index 000000000..036d2f760 --- /dev/null +++ b/src/web/assets.rs @@ -0,0 +1 @@ +//! Asset loading placeholder for the future admin Web console. diff --git a/src/web/mod.rs b/src/web/mod.rs new file mode 100644 index 000000000..58558fec1 --- /dev/null +++ b/src/web/mod.rs @@ -0,0 +1,6 @@ +//! Backend mount point for the future admin Web console. + +pub mod assets; +pub mod router; +pub mod security; +pub mod shell; diff --git a/src/web/router.rs b/src/web/router.rs new file mode 100644 index 000000000..051cff208 --- /dev/null +++ b/src/web/router.rs @@ -0,0 +1 @@ +//! Router placeholder for the future admin Web console. diff --git a/src/web/security.rs b/src/web/security.rs new file mode 100644 index 000000000..78fe5f29a --- /dev/null +++ b/src/web/security.rs @@ -0,0 +1 @@ +//! Security header placeholder for the future admin Web console. diff --git a/src/web/shell.rs b/src/web/shell.rs new file mode 100644 index 000000000..23f3e4a59 --- /dev/null +++ b/src/web/shell.rs @@ -0,0 +1 @@ +//! Shell rendering placeholder for the future admin Web console. diff --git a/tests/admin.rs b/tests/admin.rs index a074f38b9..3d838662d 100644 --- a/tests/admin.rs +++ b/tests/admin.rs @@ -2,20 +2,20 @@ mod support; mod admin { mod api_contract; - mod api_keys_route; - mod auth; - mod auth_repository; - mod login_route; + mod client_keys_route; mod logs_route; mod models_route; + mod session; + mod session_login_route; + mod session_repository; mod settings_route; mod usage_stats_route; mod accounts { mod cookies_quota; mod import_export; + mod lifecycle; mod list; - mod mutation; mod oauth; } } diff --git a/tests/admin/accounts/cookies_quota.rs b/tests/admin/accounts/cookies_quota.rs index aff4fab6f..2a99817ef 100644 --- a/tests/admin/accounts/cookies_quota.rs +++ b/tests/admin/accounts/cookies_quota.rs @@ -34,7 +34,7 @@ async fn admin_account_cookies_should_set_get_and_delete_encrypted_cookie_header .oneshot( Request::builder() .method("POST") - .uri("/admin/accounts/acct_cookies/cookies") + .uri("/api/admin/accounts/acct_cookies/cookies") .header("content-type", "application/json") .header("cookie", "cpr_admin_session=session_1") .body(Body::from( @@ -69,7 +69,7 @@ async fn admin_account_cookies_should_set_get_and_delete_encrypted_cookie_header .oneshot( Request::builder() .method("GET") - .uri("/admin/accounts/acct_cookies/cookies") + .uri("/api/admin/accounts/acct_cookies/cookies") .header("cookie", "cpr_admin_session=session_1") .body(Body::empty()) .unwrap(), @@ -88,7 +88,7 @@ async fn admin_account_cookies_should_set_get_and_delete_encrypted_cookie_header .oneshot( Request::builder() .method("DELETE") - .uri("/admin/accounts/acct_cookies/cookies") + .uri("/api/admin/accounts/acct_cookies/cookies") .header("cookie", "cpr_admin_session=session_1") .body(Body::empty()) .unwrap(), @@ -101,7 +101,7 @@ async fn admin_account_cookies_should_set_get_and_delete_encrypted_cookie_header .oneshot( Request::builder() .method("GET") - .uri("/admin/accounts/acct_cookies/cookies") + .uri("/api/admin/accounts/acct_cookies/cookies") .header("cookie", "cpr_admin_session=session_1") .body(Body::empty()) .unwrap(), @@ -123,7 +123,7 @@ async fn admin_account_cookies_should_require_existing_account_and_non_empty_coo .oneshot( Request::builder() .method("GET") - .uri("/admin/accounts/missing/cookies") + .uri("/api/admin/accounts/missing/cookies") .header("cookie", "cpr_admin_session=session_1") .body(Body::empty()) .unwrap(), @@ -137,7 +137,7 @@ async fn admin_account_cookies_should_require_existing_account_and_non_empty_coo .oneshot( Request::builder() .method("POST") - .uri("/admin/accounts/acct_cookie_invalid/cookies") + .uri("/api/admin/accounts/acct_cookie_invalid/cookies") .header("content-type", "application/json") .header("cookie", "cpr_admin_session=session_1") .body(Body::from(r#"{"cookies":""}"#)) @@ -175,7 +175,7 @@ async fn admin_account_refresh_should_update_tokens_and_runtime_pool_without_ret .oneshot( Request::builder() .method("POST") - .uri("/admin/accounts/acct_refresh/refresh") + .uri("/api/admin/accounts/acct_refresh/refresh") .header("cookie", "cpr_admin_session=session_1") .header("x-request-id", "req_refresh_account") .body(Body::empty()) @@ -229,7 +229,7 @@ async fn admin_account_refresh_should_mark_invalid_refresh_token_as_expired() { .oneshot( Request::builder() .method("POST") - .uri("/admin/accounts/acct_refresh_invalid/refresh") + .uri("/api/admin/accounts/acct_refresh_invalid/refresh") .header("cookie", "cpr_admin_session=session_1") .header("x-request-id", "req_refresh_invalid") .body(Body::empty()) @@ -275,7 +275,7 @@ async fn admin_account_reset_usage_should_clear_local_counters_and_pool_last_use .oneshot( Request::builder() .method("POST") - .uri("/admin/accounts/acct_reset_usage/reset-usage") + .uri("/api/admin/accounts/acct_reset_usage/reset-usage") .header("cookie", "cpr_admin_session=session_1") .header("x-request-id", "req_reset_usage") .body(Body::empty()) @@ -348,7 +348,7 @@ async fn admin_account_quota_should_fetch_usage_store_quota_and_not_return_secre .oneshot( Request::builder() .method("GET") - .uri("/admin/accounts/acct_quota/quota") + .uri("/api/admin/accounts/acct_quota/quota") .header("cookie", "cpr_admin_session=session_1") .header("x-request-id", "req_quota") .body(Body::empty()) @@ -386,7 +386,7 @@ async fn admin_account_quota_warnings_should_require_admin_session_cookie() { .oneshot( Request::builder() .method("GET") - .uri("/admin/accounts/quota-warnings") + .uri("/api/admin/accounts/quota-warnings") .body(Body::empty()) .unwrap(), ) @@ -449,7 +449,7 @@ async fn admin_account_quota_warnings_should_return_threshold_matches_from_cache .oneshot( Request::builder() .method("GET") - .uri("/admin/accounts/quota-warnings") + .uri("/api/admin/accounts/quota-warnings") .header("cookie", "cpr_admin_session=session_1") .body(Body::empty()) .unwrap(), @@ -520,7 +520,7 @@ async fn admin_accounts_health_check_should_probe_backend_and_mark_invalid_accou .oneshot( Request::builder() .method("POST") - .uri("/admin/accounts/health-check") + .uri("/api/admin/accounts/health-check") .header("content-type", "application/json") .header("cookie", "cpr_admin_session=session_1") .header("x-request-id", "req_health") diff --git a/tests/admin/accounts/import_export.rs b/tests/admin/accounts/import_export.rs index 234e632c9..07814445b 100644 --- a/tests/admin/accounts/import_export.rs +++ b/tests/admin/accounts/import_export.rs @@ -40,7 +40,7 @@ async fn admin_accounts_import_should_require_admin_session_cookie() { .oneshot( Request::builder() .method("POST") - .uri("/admin/accounts/import") + .uri("/api/admin/accounts/import") .header("content-type", "application/json") .header("x-request-id", "req_accounts") .body(Body::from(r#"{"accounts":[]}"#)) @@ -86,7 +86,7 @@ async fn admin_accounts_import_should_store_tokens_encrypted_and_list_sanitized_ .oneshot( Request::builder() .method("POST") - .uri("/admin/accounts/import") + .uri("/api/admin/accounts/import") .header("content-type", "application/json") .header("cookie", "cpr_admin_session=session_1") .header("x-request-id", "req_accounts") @@ -116,7 +116,7 @@ async fn admin_accounts_import_should_store_tokens_encrypted_and_list_sanitized_ .oneshot( Request::builder() .method("GET") - .uri("/admin/accounts?limit=10") + .uri("/api/admin/accounts?limit=10") .header("cookie", "cpr_admin_session=session_1") .header("x-request-id", "req_accounts_list") .body(Body::empty()) @@ -181,7 +181,7 @@ async fn admin_accounts_import_should_accept_sub2api_oauth_export_and_mark_forma .oneshot( Request::builder() .method("POST") - .uri("/admin/accounts/import") + .uri("/api/admin/accounts/import") .header("content-type", "application/json") .header("cookie", "cpr_admin_session=session_1") .header("x-request-id", "req_sub2api") @@ -248,7 +248,7 @@ async fn admin_accounts_import_should_accept_sub2api_native_account_export_witho .oneshot( Request::builder() .method("POST") - .uri("/admin/accounts/import") + .uri("/api/admin/accounts/import") .header("content-type", "application/json") .header("cookie", "cpr_admin_session=session_1") .header("x-request-id", "req_sub2api_native") @@ -267,7 +267,7 @@ async fn admin_accounts_import_should_accept_sub2api_native_account_export_witho .oneshot( Request::builder() .method("GET") - .uri("/admin/accounts?limit=10") + .uri("/api/admin/accounts?limit=10") .header("cookie", "cpr_admin_session=session_1") .header("x-request-id", "req_sub2api_native_list") .body(Body::empty()) @@ -294,7 +294,7 @@ async fn admin_accounts_export_should_return_native_accounts_with_tokens_and_fil .oneshot( Request::builder() .method("GET") - .uri("/admin/accounts/export?ids=acct_export_a") + .uri("/api/admin/accounts/export?ids=acct_export_a") .header("cookie", "cpr_admin_session=session_1") .body(Body::empty()) .unwrap(), @@ -317,7 +317,7 @@ async fn admin_accounts_export_should_return_native_accounts_with_tokens_and_fil .oneshot( Request::builder() .method("GET") - .uri("/admin/accounts/export?format=proxy") + .uri("/api/admin/accounts/export?format=proxy") .header("cookie", "cpr_admin_session=session_1") .body(Body::empty()) .unwrap(), @@ -338,7 +338,7 @@ async fn admin_accounts_export_should_return_sub2api_openai_oauth_payload_withou .oneshot( Request::builder() .method("PATCH") - .uri("/admin/accounts/acct_export_sub2api/label") + .uri("/api/admin/accounts/acct_export_sub2api/label") .header("content-type", "application/json") .header("cookie", "cpr_admin_session=session_1") .body(Body::from(r#"{"label":"Sub2API Export"}"#)) @@ -352,7 +352,7 @@ async fn admin_accounts_export_should_return_sub2api_openai_oauth_payload_withou .oneshot( Request::builder() .method("GET") - .uri("/admin/accounts/export?format=sub2api") + .uri("/api/admin/accounts/export?format=sub2api") .header("cookie", "cpr_admin_session=session_1") .body(Body::empty()) .unwrap(), @@ -405,7 +405,7 @@ async fn manual_add_should_derive_claims_ignore_metadata_and_sync_pool() { .oneshot( Request::builder() .method("POST") - .uri("/admin/accounts") + .uri("/api/admin/accounts") .header("content-type", "application/json") .header("cookie", "cpr_admin_session=session_1") .body(Body::from( @@ -494,7 +494,7 @@ async fn admin_account_manual_add_should_reject_missing_invalid_expired_or_unbou .oneshot( Request::builder() .method("POST") - .uri("/admin/accounts") + .uri("/api/admin/accounts") .header("content-type", "application/json") .header("cookie", "cpr_admin_session=session_1") .body(Body::from(r#"{}"#)) @@ -509,7 +509,7 @@ async fn admin_account_manual_add_should_reject_missing_invalid_expired_or_unbou .oneshot( Request::builder() .method("POST") - .uri("/admin/accounts") + .uri("/api/admin/accounts") .header("content-type", "application/json") .header("cookie", "cpr_admin_session=session_1") .body(Body::from( @@ -529,7 +529,7 @@ async fn admin_account_manual_add_should_reject_missing_invalid_expired_or_unbou .oneshot( Request::builder() .method("POST") - .uri("/admin/accounts") + .uri("/api/admin/accounts") .header("content-type", "application/json") .header("cookie", "cpr_admin_session=session_1") .body(Body::from( @@ -554,7 +554,7 @@ async fn admin_account_manual_add_should_reject_missing_invalid_expired_or_unbou .oneshot( Request::builder() .method("POST") - .uri("/admin/accounts") + .uri("/api/admin/accounts") .header("content-type", "application/json") .header("cookie", "cpr_admin_session=session_1") .body(Body::from( @@ -822,7 +822,7 @@ async fn admin_accounts_import_cli_should_read_codex_auth_file_store_encrypted_a .oneshot( Request::builder() .method("POST") - .uri("/admin/accounts/import-cli") + .uri("/api/admin/accounts/import-cli") .header("content-type", "application/json") .header("cookie", "cpr_admin_session=session_1") .header("x-request-id", "req_import_cli") diff --git a/tests/admin/accounts/mutation.rs b/tests/admin/accounts/lifecycle.rs similarity index 93% rename from tests/admin/accounts/mutation.rs rename to tests/admin/accounts/lifecycle.rs index d37a8ba6f..cda699a0d 100644 --- a/tests/admin/accounts/mutation.rs +++ b/tests/admin/accounts/lifecycle.rs @@ -20,7 +20,7 @@ async fn admin_account_label_should_update_and_clear_label() { .oneshot( Request::builder() .method("PATCH") - .uri("/admin/accounts/acct_label/label") + .uri("/api/admin/accounts/acct_label/label") .header("content-type", "application/json") .header("cookie", "cpr_admin_session=session_1") .header("x-request-id", "req_label") @@ -37,7 +37,7 @@ async fn admin_account_label_should_update_and_clear_label() { .oneshot( Request::builder() .method("PATCH") - .uri("/admin/accounts/acct_label/label") + .uri("/api/admin/accounts/acct_label/label") .header("content-type", "application/json") .header("cookie", "cpr_admin_session=session_1") .body(Body::from(r#"{"label":null}"#)) @@ -75,7 +75,7 @@ async fn admin_account_label_should_reject_too_long_or_missing_account() { .oneshot( Request::builder() .method("PATCH") - .uri("/admin/accounts/acct_label_invalid/label") + .uri("/api/admin/accounts/acct_label_invalid/label") .header("content-type", "application/json") .header("cookie", "cpr_admin_session=session_1") .body(Body::from(json!({ "label": long_label }).to_string())) @@ -89,7 +89,7 @@ async fn admin_account_label_should_reject_too_long_or_missing_account() { .oneshot( Request::builder() .method("PATCH") - .uri("/admin/accounts/missing/label") + .uri("/api/admin/accounts/missing/label") .header("content-type", "application/json") .header("cookie", "cpr_admin_session=session_1") .body(Body::from(r#"{"label":"Team Alpha"}"#)) @@ -109,7 +109,7 @@ async fn admin_account_status_should_update_database_and_runtime_pool() { .oneshot( Request::builder() .method("PATCH") - .uri("/admin/accounts/acct_status/status") + .uri("/api/admin/accounts/acct_status/status") .header("content-type", "application/json") .header("cookie", "cpr_admin_session=session_1") .header("x-request-id", "req_status") @@ -146,7 +146,7 @@ async fn admin_account_delete_should_remove_database_row_and_runtime_pool_entry( .oneshot( Request::builder() .method("DELETE") - .uri("/admin/accounts/acct_delete") + .uri("/api/admin/accounts/acct_delete") .header("cookie", "cpr_admin_session=session_1") .header("x-request-id", "req_delete") .body(Body::empty()) @@ -175,7 +175,7 @@ async fn admin_account_delete_should_remove_database_row_and_runtime_pool_entry( .oneshot( Request::builder() .method("DELETE") - .uri("/admin/accounts/acct_delete") + .uri("/api/admin/accounts/acct_delete") .header("cookie", "cpr_admin_session=session_1") .body(Body::empty()) .unwrap(), @@ -197,7 +197,7 @@ async fn admin_accounts_batch_delete_should_delete_found_accounts_and_report_mis .oneshot( Request::builder() .method("POST") - .uri("/admin/accounts/batch-delete") + .uri("/api/admin/accounts/batch-delete") .header("content-type", "application/json") .header("cookie", "cpr_admin_session=session_1") .body(Body::from( @@ -231,7 +231,7 @@ async fn admin_accounts_batch_delete_should_delete_found_accounts_and_report_mis .oneshot( Request::builder() .method("POST") - .uri("/admin/accounts/batch-delete") + .uri("/api/admin/accounts/batch-delete") .header("content-type", "application/json") .header("cookie", "cpr_admin_session=session_1") .body(Body::from(r#"{"ids":[]}"#)) @@ -254,7 +254,7 @@ async fn admin_accounts_batch_status_should_update_found_accounts_and_reject_inv .oneshot( Request::builder() .method("POST") - .uri("/admin/accounts/batch-status") + .uri("/api/admin/accounts/batch-status") .header("content-type", "application/json") .header("cookie", "cpr_admin_session=session_1") .body(Body::from( @@ -298,7 +298,7 @@ async fn admin_accounts_batch_status_should_update_found_accounts_and_reject_inv .oneshot( Request::builder() .method("POST") - .uri("/admin/accounts/batch-status") + .uri("/api/admin/accounts/batch-status") .header("content-type", "application/json") .header("cookie", "cpr_admin_session=session_1") .body(Body::from( @@ -318,7 +318,7 @@ async fn admin_accounts_batch_status_should_update_found_accounts_and_reject_inv .oneshot( Request::builder() .method("POST") - .uri("/admin/accounts/batch-status") + .uri("/api/admin/accounts/batch-status") .header("content-type", "application/json") .header("cookie", "cpr_admin_session=session_1") .body(Body::from(r#"{"ids":[],"status":"active"}"#)) diff --git a/tests/admin/accounts/list.rs b/tests/admin/accounts/list.rs index 6305489d3..79d7febe0 100644 --- a/tests/admin/accounts/list.rs +++ b/tests/admin/accounts/list.rs @@ -42,7 +42,7 @@ async fn admin_accounts_list_should_not_decrypt_account_tokens() { .oneshot( Request::builder() .method("GET") - .uri("/admin/accounts?limit=10") + .uri("/api/admin/accounts?limit=10") .header("cookie", "cpr_admin_session=session_1") .header("x-request-id", "req_accounts_list") .body(Body::empty()) diff --git a/tests/admin/accounts/oauth.rs b/tests/admin/accounts/oauth.rs index edc1cce90..947bcec16 100644 --- a/tests/admin/accounts/oauth.rs +++ b/tests/admin/accounts/oauth.rs @@ -32,7 +32,7 @@ async fn admin_auth_status_should_require_admin_session_cookie() { .oneshot( Request::builder() .method("GET") - .uri("/admin/auth/status") + .uri("/api/admin/auth/status") .header("x-request-id", "req_auth_status_no_session") .body(Body::empty()) .unwrap(), @@ -55,7 +55,7 @@ async fn admin_auth_status_should_report_empty_account_pool() { .oneshot( Request::builder() .method("GET") - .uri("/admin/auth/status") + .uri("/api/admin/auth/status") .header("cookie", "cpr_admin_session=session_1") .header("x-request-id", "req_auth_status_empty") .body(Body::empty()) @@ -88,7 +88,7 @@ async fn admin_auth_status_should_report_user_and_pool_summary_without_secrets() .oneshot( Request::builder() .method("GET") - .uri("/admin/auth/status") + .uri("/api/admin/auth/status") .header("cookie", "cpr_admin_session=session_1") .header("x-request-id", "req_auth_status_summary") .body(Body::empty()) @@ -124,7 +124,7 @@ async fn admin_auth_logout_should_clear_accounts_and_runtime_pool() { .oneshot( Request::builder() .method("POST") - .uri("/admin/accounts/acct_auth_logout/cookies") + .uri("/api/admin/accounts/acct_auth_logout/cookies") .header("content-type", "application/json") .header("cookie", "cpr_admin_session=session_1") .body(Body::from(r#"{"cookies":"cf_clearance=secret"}"#)) @@ -138,7 +138,7 @@ async fn admin_auth_logout_should_clear_accounts_and_runtime_pool() { .oneshot( Request::builder() .method("POST") - .uri("/admin/auth/logout") + .uri("/api/admin/auth/logout") .header("cookie", "cpr_admin_session=session_1") .header("x-request-id", "req_auth_logout") .body(Body::empty()) @@ -183,7 +183,7 @@ async fn admin_auth_device_login_should_require_admin_session_cookie() { .oneshot( Request::builder() .method("POST") - .uri("/admin/auth/device-login") + .uri("/api/admin/auth/device-login") .header("x-request-id", "req_device_login_no_session") .body(Body::empty()) .unwrap(), @@ -222,7 +222,7 @@ async fn admin_auth_device_login_should_return_openai_device_code() { .oneshot( Request::builder() .method("POST") - .uri("/admin/auth/device-login") + .uri("/api/admin/auth/device-login") .header("cookie", "cpr_admin_session=session_1") .header("x-request-id", "req_device_login") .body(Body::empty()) @@ -261,7 +261,7 @@ async fn admin_auth_device_poll_should_return_pending_without_importing_account( .oneshot( Request::builder() .method("GET") - .uri("/admin/auth/device-poll/device-secret") + .uri("/api/admin/auth/device-poll/device-secret") .header("cookie", "cpr_admin_session=session_1") .header("x-request-id", "req_device_pending") .body(Body::empty()) @@ -311,7 +311,7 @@ async fn admin_auth_device_poll_should_import_tokens_without_returning_secrets() .oneshot( Request::builder() .method("GET") - .uri("/admin/auth/device-poll/device-success") + .uri("/api/admin/auth/device-poll/device-success") .header("cookie", "cpr_admin_session=session_1") .header("x-request-id", "req_device_success") .body(Body::empty()) @@ -367,7 +367,7 @@ async fn admin_auth_login_start_should_require_admin_session_cookie() { .oneshot( Request::builder() .method("POST") - .uri("/admin/auth/login-start") + .uri("/api/admin/auth/login-start") .header("x-request-id", "req_login_start_no_session") .body(Body::empty()) .unwrap(), @@ -390,7 +390,7 @@ async fn admin_auth_login_start_should_return_pkce_auth_url_and_state() { .oneshot( Request::builder() .method("POST") - .uri("/admin/auth/login-start") + .uri("/api/admin/auth/login-start") .header("cookie", "cpr_admin_session=session_1") .header("host", "127.0.0.1:8080") .header("x-request-id", "req_login_start") @@ -408,7 +408,9 @@ async fn admin_auth_login_start_should_return_pkce_auth_url_and_state() { assert!(auth_url.starts_with("https://auth.openai.com/oauth/authorize?")); assert!(auth_url.contains("response_type=code")); assert!(auth_url.contains("client_id=app_EMoamEEZ73f0CkXaXp7hrann")); - assert!(auth_url.contains("redirect_uri=http%3A%2F%2Flocalhost%3A1455%2Fauth%2Fcallback")); + assert!( + auth_url.contains("redirect_uri=http%3A%2F%2Flocalhost%3A1455%2Fauth%2Fopenai%2Fcallback") + ); assert!(auth_url.contains("scope=openid%20profile%20email%20offline_access")); assert!(auth_url.contains("code_challenge_method=S256")); assert!(auth_url.contains("originator=codex_cli_rs")); @@ -442,7 +444,7 @@ async fn admin_auth_code_relay_should_exchange_code_and_import_account() { .oneshot( Request::builder() .method("POST") - .uri("/admin/auth/login-start") + .uri("/api/admin/auth/login-start") .header("cookie", "cpr_admin_session=session_1") .header("host", "127.0.0.1:8080") .body(Body::empty()) @@ -453,13 +455,13 @@ async fn admin_auth_code_relay_should_exchange_code_and_import_account() { let start_body = response_json(start).await; let state_value = start_body["data"]["state"].as_str().unwrap(); let callback_url = - format!("http://localhost:1455/auth/callback?code=oauth-code&state={state_value}"); + format!("http://localhost:1455/auth/openai/callback?code=oauth-code&state={state_value}"); let response = app .oneshot( Request::builder() .method("POST") - .uri("/admin/auth/code-relay") + .uri("/api/admin/auth/code-relay") .header("content-type", "application/json") .header("cookie", "cpr_admin_session=session_1") .header("x-request-id", "req_code_relay") @@ -480,7 +482,10 @@ async fn admin_auth_code_relay_should_exchange_code_and_import_account() { assert_eq!(calls.len(), 1); assert_eq!(calls[0].code, "oauth-code"); assert!(!calls[0].code_verifier.is_empty()); - assert_eq!(calls[0].redirect_uri, "http://localhost:1455/auth/callback"); + assert_eq!( + calls[0].redirect_uri, + "http://localhost:1455/auth/openai/callback" + ); drop(calls); let stored = AccountRepository::new(pool.clone(), SecretBox::new([43; 32])) .find_by_chatgpt_identity("pkce-account", Some("pkce-user")) @@ -521,7 +526,7 @@ async fn admin_auth_code_relay_should_reject_invalid_callback_url() { .oneshot( Request::builder() .method("POST") - .uri("/admin/auth/code-relay") + .uri("/api/admin/auth/code-relay") .header("content-type", "application/json") .header("cookie", "cpr_admin_session=session_1") .header("x-request-id", "req_code_relay_invalid") @@ -565,7 +570,7 @@ async fn admin_auth_callback_should_exchange_code_and_redirect_to_return_host() .oneshot( Request::builder() .method("POST") - .uri("/admin/auth/login-start") + .uri("/api/admin/auth/login-start") .header("cookie", "cpr_admin_session=session_1") .header("host", "codex.local:1455") .body(Body::empty()) @@ -575,7 +580,7 @@ async fn admin_auth_callback_should_exchange_code_and_redirect_to_return_host() .unwrap(); let start_body = response_json(start).await; let state_value = start_body["data"]["state"].as_str().unwrap(); - let callback_path = format!("/auth/callback?code=callback-code&state={state_value}"); + let callback_path = format!("/auth/openai/callback?code=callback-code&state={state_value}"); let response = app .oneshot( diff --git a/tests/admin/api_contract.rs b/tests/admin/api_contract.rs index 8d8daff77..1a807ff81 100644 --- a/tests/admin/api_contract.rs +++ b/tests/admin/api_contract.rs @@ -6,7 +6,7 @@ use axum::{ use serde_json::{json, Value}; use codex_proxy_rs::{ - admin::http::{AdminEnvelope, AdminError, AdminPageEnvelope, AdminResponse}, + admin::api::{AdminEnvelope, AdminError, AdminPageEnvelope, AdminResponse}, utils::pagination::Page, }; diff --git a/tests/admin/api_keys_route.rs b/tests/admin/client_keys_route.rs similarity index 94% rename from tests/admin/api_keys_route.rs rename to tests/admin/client_keys_route.rs index c84e07dec..0cbb36cb9 100644 --- a/tests/admin/api_keys_route.rs +++ b/tests/admin/client_keys_route.rs @@ -14,7 +14,7 @@ use codex_proxy_rs::{ UsageStatsConfig, }, platform::crypto::SecretBox, - platform::identity::api_key::ApiKeyHasher, + platform::identity::client_key::ApiKeyHasher, platform::storage::db::connect_sqlite, runtime::build_router, runtime::state::AppState, @@ -114,7 +114,7 @@ async fn admin_api_keys_should_create_list_and_authorize_v1_requests() { .oneshot( Request::builder() .method("POST") - .uri("/admin/api-keys") + .uri("/api/admin/api-keys") .header("content-type", "application/json") .header("cookie", "cpr_admin_session=session_1") .header("x-request-id", "req_api_key") @@ -134,7 +134,7 @@ async fn admin_api_keys_should_create_list_and_authorize_v1_requests() { .oneshot( Request::builder() .method("GET") - .uri("/admin/api-keys?limit=10") + .uri("/api/admin/api-keys?limit=10") .header("cookie", "cpr_admin_session=session_1") .body(Body::empty()) .unwrap(), @@ -171,7 +171,7 @@ async fn admin_api_key_status_should_disable_and_reenable_client_key_authorizati .oneshot( Request::builder() .method("PATCH") - .uri(format!("/admin/api-keys/{key_id}/status")) + .uri(format!("/api/admin/api-keys/{key_id}/status")) .header("content-type", "application/json") .header("cookie", "cpr_admin_session=session_1") .body(Body::from(r#"{"status":"disabled"}"#)) @@ -202,7 +202,7 @@ async fn admin_api_key_status_should_disable_and_reenable_client_key_authorizati .oneshot( Request::builder() .method("PATCH") - .uri(format!("/admin/api-keys/{key_id}/status")) + .uri(format!("/api/admin/api-keys/{key_id}/status")) .header("content-type", "application/json") .header("cookie", "cpr_admin_session=session_1") .body(Body::from(r#"{"status":"active"}"#)) @@ -232,7 +232,7 @@ async fn admin_api_key_status_should_disable_and_reenable_client_key_authorizati .oneshot( Request::builder() .method("PATCH") - .uri(format!("/admin/api-keys/{key_id}/status")) + .uri(format!("/api/admin/api-keys/{key_id}/status")) .header("content-type", "application/json") .header("cookie", "cpr_admin_session=session_1") .body(Body::from(r#"{"status":"expired"}"#)) @@ -253,7 +253,7 @@ async fn admin_api_key_delete_should_remove_client_key_authorization() { .oneshot( Request::builder() .method("DELETE") - .uri(format!("/admin/api-keys/{key_id}")) + .uri(format!("/api/admin/api-keys/{key_id}")) .header("cookie", "cpr_admin_session=session_1") .body(Body::empty()) .unwrap(), @@ -269,7 +269,7 @@ async fn admin_api_key_delete_should_remove_client_key_authorization() { .oneshot( Request::builder() .method("GET") - .uri("/admin/api-keys?limit=10") + .uri("/api/admin/api-keys?limit=10") .header("cookie", "cpr_admin_session=session_1") .body(Body::empty()) .unwrap(), @@ -298,7 +298,7 @@ async fn admin_api_key_delete_should_remove_client_key_authorization() { .oneshot( Request::builder() .method("DELETE") - .uri(format!("/admin/api-keys/{key_id}")) + .uri(format!("/api/admin/api-keys/{key_id}")) .header("cookie", "cpr_admin_session=session_1") .body(Body::empty()) .unwrap(), @@ -318,7 +318,7 @@ async fn admin_api_key_label_should_update_and_clear_local_client_key_name() { .oneshot( Request::builder() .method("PATCH") - .uri(format!("/admin/api-keys/{key_id}/label")) + .uri(format!("/api/admin/api-keys/{key_id}/label")) .header("content-type", "application/json") .header("cookie", "cpr_admin_session=session_1") .body(Body::from(r#"{"label":"automation"}"#)) @@ -336,7 +336,7 @@ async fn admin_api_key_label_should_update_and_clear_local_client_key_name() { .oneshot( Request::builder() .method("PATCH") - .uri(format!("/admin/api-keys/{key_id}/label")) + .uri(format!("/api/admin/api-keys/{key_id}/label")) .header("content-type", "application/json") .header("cookie", "cpr_admin_session=session_1") .body(Body::from(r#"{"label":null}"#)) @@ -354,7 +354,7 @@ async fn admin_api_key_label_should_update_and_clear_local_client_key_name() { .oneshot( Request::builder() .method("PATCH") - .uri(format!("/admin/api-keys/{key_id}/label")) + .uri(format!("/api/admin/api-keys/{key_id}/label")) .header("content-type", "application/json") .header("cookie", "cpr_admin_session=session_1") .body(Body::from(json!({ "label": "x".repeat(65) }).to_string())) @@ -368,7 +368,7 @@ async fn admin_api_key_label_should_update_and_clear_local_client_key_name() { .oneshot( Request::builder() .method("PATCH") - .uri("/admin/api-keys/missing/label") + .uri("/api/admin/api-keys/missing/label") .header("content-type", "application/json") .header("cookie", "cpr_admin_session=session_1") .body(Body::from(r#"{"label":"automation"}"#)) @@ -390,7 +390,7 @@ async fn admin_api_keys_batch_delete_should_remove_found_keys_and_report_missing .oneshot( Request::builder() .method("POST") - .uri("/admin/api-keys/batch-delete") + .uri("/api/admin/api-keys/batch-delete") .header("content-type", "application/json") .header("cookie", "cpr_admin_session=session_1") .body(Body::from( @@ -428,7 +428,7 @@ async fn admin_api_keys_batch_delete_should_remove_found_keys_and_report_missing .oneshot( Request::builder() .method("POST") - .uri("/admin/api-keys/batch-delete") + .uri("/api/admin/api-keys/batch-delete") .header("content-type", "application/json") .header("cookie", "cpr_admin_session=session_1") .body(Body::from(r#"{"ids":[]}"#)) @@ -464,7 +464,7 @@ async fn create_admin_api_key( .oneshot( Request::builder() .method("POST") - .uri("/admin/api-keys") + .uri("/api/admin/api-keys") .header("content-type", "application/json") .header("cookie", format!("cpr_admin_session={session_id}")) .body(Body::from(json!({ "name": name }).to_string())) @@ -489,7 +489,7 @@ async fn admin_api_keys_export_should_return_metadata_without_secret_material() .oneshot( Request::builder() .method("GET") - .uri(format!("/admin/api-keys/export?ids={key_id}")) + .uri(format!("/api/admin/api-keys/export?ids={key_id}")) .header("cookie", "cpr_admin_session=session_1") .header("x-request-id", "req_api_key_export") .body(Body::empty()) @@ -535,7 +535,7 @@ async fn admin_api_keys_import_should_rotate_exported_metadata_and_return_new_pl .oneshot( Request::builder() .method("GET") - .uri("/admin/api-keys/export") + .uri("/api/admin/api-keys/export") .header("cookie", "cpr_admin_session=session_1") .body(Body::empty()) .unwrap(), @@ -562,7 +562,7 @@ async fn admin_api_keys_import_should_rotate_exported_metadata_and_return_new_pl .oneshot( Request::builder() .method("POST") - .uri("/admin/api-keys/import") + .uri("/api/admin/api-keys/import") .header("content-type", "application/json") .header("cookie", "cpr_admin_session=session_1") .header("x-request-id", "req_api_key_import") @@ -593,7 +593,7 @@ async fn admin_api_keys_import_should_rotate_exported_metadata_and_return_new_pl .oneshot( Request::builder() .method("GET") - .uri("/admin/api-keys?limit=10") + .uri("/api/admin/api-keys?limit=10") .header("cookie", "cpr_admin_session=session_1") .body(Body::empty()) .unwrap(), diff --git a/tests/admin/logs_route.rs b/tests/admin/logs_route.rs index 64eb68990..e0b562f02 100644 --- a/tests/admin/logs_route.rs +++ b/tests/admin/logs_route.rs @@ -7,7 +7,7 @@ use axum::{ use tower::ServiceExt; use codex_proxy_rs::{ - codex::logs::{ + codex::events::{ event::{EventLevel, EventLog}, repository::EventLogRepository, }, @@ -102,7 +102,7 @@ async fn admin_logs_are_cursor_paginated_and_include_request_id() { .oneshot( Request::builder() .method("GET") - .uri("/admin/logs?limit=1") + .uri("/api/admin/logs?limit=1") .header("cookie", "cpr_admin_session=session_1") .header("x-request-id", "req_admin") .body(Body::empty()) @@ -152,7 +152,7 @@ async fn admin_logs_should_filter_by_kind_level_and_search_text() { .oneshot( Request::builder() .method("GET") - .uri("/admin/logs?kind=request&level=error&search=timeout") + .uri("/api/admin/logs?kind=request&level=error&search=timeout") .header("cookie", "cpr_admin_session=session_1") .header("x-request-id", "req_logs_filter") .body(Body::empty()) @@ -182,7 +182,7 @@ async fn admin_logs_reject_missing_admin_session_cookie() { .oneshot( Request::builder() .method("GET") - .uri("/admin/logs") + .uri("/api/admin/logs") .header("x-request-id", "req_admin") .body(Body::empty()) .unwrap(), @@ -216,7 +216,7 @@ async fn admin_logs_state_should_return_runtime_logging_state_and_stored_count() .oneshot( Request::builder() .method("GET") - .uri("/admin/logs/state") + .uri("/api/admin/logs/state") .header("cookie", "cpr_admin_session=session_1") .header("x-request-id", "req_logs_state") .body(Body::empty()) @@ -247,7 +247,7 @@ async fn admin_logs_state_patch_should_require_admin_session_cookie() { .oneshot( Request::builder() .method("PATCH") - .uri("/admin/logs/state") + .uri("/api/admin/logs/state") .header("content-type", "application/json") .header("x-request-id", "req_logs_state_patch") .body(Body::from( @@ -278,7 +278,7 @@ async fn admin_logs_state_patch_should_update_runtime_logging_state() { .oneshot( Request::builder() .method("PATCH") - .uri("/admin/logs/state") + .uri("/api/admin/logs/state") .header("content-type", "application/json") .header("cookie", "cpr_admin_session=session_1") .header("x-request-id", "req_logs_state_patch") @@ -307,7 +307,7 @@ async fn admin_logs_state_patch_should_update_runtime_logging_state() { .oneshot( Request::builder() .method("GET") - .uri("/admin/logs/state") + .uri("/api/admin/logs/state") .header("cookie", "cpr_admin_session=session_1") .body(Body::empty()) .unwrap(), @@ -339,7 +339,7 @@ async fn admin_logs_detail_should_return_one_event_by_id() { .oneshot( Request::builder() .method("GET") - .uri("/admin/logs/log_detail") + .uri("/api/admin/logs/log_detail") .header("cookie", "cpr_admin_session=session_1") .header("x-request-id", "req_logs_detail") .body(Body::empty()) @@ -379,7 +379,7 @@ async fn admin_logs_clear_should_delete_stored_events() { .oneshot( Request::builder() .method("DELETE") - .uri("/admin/logs") + .uri("/api/admin/logs") .header("cookie", "cpr_admin_session=session_1") .header("x-request-id", "req_logs_clear") .body(Body::empty()) @@ -398,7 +398,7 @@ async fn admin_logs_clear_should_delete_stored_events() { .oneshot( Request::builder() .method("GET") - .uri("/admin/logs?limit=50") + .uri("/api/admin/logs?limit=50") .header("cookie", "cpr_admin_session=session_1") .body(Body::empty()) .unwrap(), diff --git a/tests/admin/models_route.rs b/tests/admin/models_route.rs index da5cd582a..1e3c4a81a 100644 --- a/tests/admin/models_route.rs +++ b/tests/admin/models_route.rs @@ -13,18 +13,18 @@ use wiremock::{ }; use codex_proxy_rs::{ - codex::accounts::models::repository::ModelSnapshotRepository, codex::accounts::{ model::{Account, AccountStatus}, repository::{AccountRepository, NewAccount}, }, + codex::models::repository::ModelSnapshotRepository, config::{ AdminConfig, ApiConfig, AppConfig, AuthConfig, DatabaseConfig, LoggingConfig, ModelConfig, QuotaConfig, QuotaWarningThresholds, SecurityConfig, ServerConfig, TlsConfig, UsageStatsConfig, }, platform::crypto::SecretBox, - platform::identity::{api_key::ApiKeyHasher, api_key_repository::ClientApiKeyRepository}, + platform::identity::{client_key::ApiKeyHasher, client_key_repository::ClientApiKeyRepository}, platform::storage::db::connect_sqlite, runtime::build_router, runtime::state::AppState, @@ -106,7 +106,7 @@ async fn admin_refresh_models_should_require_admin_session_cookie() { .oneshot( Request::builder() .method("POST") - .uri("/admin/refresh-models") + .uri("/api/admin/refresh-models") .header("x-request-id", "req_refresh_models") .body(Body::empty()) .unwrap(), @@ -185,7 +185,7 @@ async fn admin_refresh_models_should_fetch_snapshots_for_distinct_account_plans( .oneshot( Request::builder() .method("POST") - .uri("/admin/refresh-models") + .uri("/api/admin/refresh-models") .header("cookie", "cpr_admin_session=session_1") .header("x-request-id", "req_refresh_models") .body(Body::empty()) diff --git a/tests/admin/auth.rs b/tests/admin/session.rs similarity index 100% rename from tests/admin/auth.rs rename to tests/admin/session.rs diff --git a/tests/admin/login_route.rs b/tests/admin/session_login_route.rs similarity index 97% rename from tests/admin/login_route.rs rename to tests/admin/session_login_route.rs index 0e4099f45..ce3d03231 100644 --- a/tests/admin/login_route.rs +++ b/tests/admin/session_login_route.rs @@ -93,7 +93,7 @@ async fn admin_login_issues_http_only_session_cookie() { .oneshot( Request::builder() .method("POST") - .uri("/admin/login") + .uri("/api/admin/login") .header("content-type", "application/json") .header("x-request-id", "req_login") .body(Body::from(r#"{"password":"correct-password"}"#)) @@ -123,7 +123,7 @@ async fn admin_login_issues_http_only_session_cookie() { .oneshot( Request::builder() .method("GET") - .uri("/admin/logs") + .uri("/api/admin/logs") .header("cookie", session_cookie) .body(Body::empty()) .unwrap(), @@ -147,7 +147,7 @@ async fn admin_login_rejects_client_api_key_as_password_or_authorization() { .oneshot( Request::builder() .method("POST") - .uri("/admin/login") + .uri("/api/admin/login") .header("content-type", "application/json") .header("authorization", "Bearer cpr_not_an_admin_session") .header("x-request-id", "req_login_bad") @@ -167,7 +167,7 @@ async fn admin_login_rejects_client_api_key_as_password_or_authorization() { .oneshot( Request::builder() .method("GET") - .uri("/admin/logs") + .uri("/api/admin/logs") .header("authorization", "Bearer cpr_not_an_admin_session") .body(Body::empty()) .unwrap(), diff --git a/tests/admin/auth_repository.rs b/tests/admin/session_repository.rs similarity index 97% rename from tests/admin/auth_repository.rs rename to tests/admin/session_repository.rs index 5e045d887..7c41afcaf 100644 --- a/tests/admin/auth_repository.rs +++ b/tests/admin/session_repository.rs @@ -1,7 +1,7 @@ use chrono::{Duration, Utc}; use codex_proxy_rs::{ - admin::auth::repository::AdminAuthRepository, + admin::session::repository::AdminAuthRepository, platform::{identity::admin_session::hash_admin_password, storage::db::connect_sqlite}, }; diff --git a/tests/admin/settings_route.rs b/tests/admin/settings_route.rs index 27780579d..1e01d1690 100644 --- a/tests/admin/settings_route.rs +++ b/tests/admin/settings_route.rs @@ -101,7 +101,7 @@ async fn admin_settings_should_require_admin_session_cookie() { .oneshot( Request::builder() .method("GET") - .uri("/admin/settings") + .uri("/api/admin/settings") .header("x-request-id", "req_settings") .body(Body::empty()) .unwrap(), @@ -127,7 +127,7 @@ async fn admin_settings_patch_should_require_admin_session_cookie() { .oneshot( Request::builder() .method("PATCH") - .uri("/admin/settings") + .uri("/api/admin/settings") .header("content-type", "application/json") .header("x-request-id", "req_settings_patch_auth") .body(Body::from(r#"{"defaultModel":"gpt-5.5"}"#)) @@ -162,7 +162,7 @@ async fn admin_settings_patch_should_persist_retained_fields_to_local_yaml() { .oneshot( Request::builder() .method("PATCH") - .uri("/admin/settings") + .uri("/api/admin/settings") .header("content-type", "application/json") .header("cookie", "cpr_admin_session=session_1") .header("x-request-id", "req_settings_patch") @@ -227,7 +227,7 @@ async fn admin_settings_patch_should_persist_retained_fields_to_local_yaml() { .oneshot( Request::builder() .method("GET") - .uri("/admin/settings") + .uri("/api/admin/settings") .header("cookie", "cpr_admin_session=session_1") .body(Body::empty()) .unwrap(), @@ -258,7 +258,7 @@ async fn admin_settings_patch_should_reject_removed_or_invalid_fields() { .oneshot( Request::builder() .method("PATCH") - .uri("/admin/settings") + .uri("/api/admin/settings") .header("content-type", "application/json") .header("cookie", "cpr_admin_session=session_1") .header("x-request-id", "req_settings_patch_invalid") @@ -294,7 +294,7 @@ async fn admin_settings_should_return_only_in_scope_runtime_fields() { .oneshot( Request::builder() .method("GET") - .uri("/admin/settings") + .uri("/api/admin/settings") .header("cookie", "cpr_admin_session=session_1") .header("x-request-id", "req_settings") .body(Body::empty()) diff --git a/tests/admin/usage_stats_route.rs b/tests/admin/usage_stats_route.rs index 1579a9ee2..1d40b2fae 100644 --- a/tests/admin/usage_stats_route.rs +++ b/tests/admin/usage_stats_route.rs @@ -94,7 +94,7 @@ async fn admin_usage_stats_should_cursor_page_account_usage() { .oneshot( Request::builder() .method("GET") - .uri("/admin/usage-stats?limit=1") + .uri("/api/admin/usage-stats?limit=1") .header("cookie", "cpr_admin_session=session_1") .header("x-request-id", "req_usage") .body(Body::empty()) @@ -119,7 +119,7 @@ async fn admin_usage_stats_should_cursor_page_account_usage() { .oneshot( Request::builder() .method("GET") - .uri(format!("/admin/usage-stats?limit=1&cursor={cursor}")) + .uri(format!("/api/admin/usage-stats?limit=1&cursor={cursor}")) .header("cookie", "cpr_admin_session=session_1") .body(Body::empty()) .unwrap(), @@ -147,7 +147,7 @@ async fn admin_usage_stats_summary_should_return_usage_totals() { .oneshot( Request::builder() .method("GET") - .uri("/admin/usage-stats/summary") + .uri("/api/admin/usage-stats/summary") .header("cookie", "cpr_admin_session=session_1") .header("x-request-id", "req_usage_summary") .body(Body::empty()) @@ -179,7 +179,7 @@ async fn admin_usage_stats_should_reject_missing_admin_session_cookie() { .oneshot( Request::builder() .method("GET") - .uri("/admin/usage-stats") + .uri("/api/admin/usage-stats") .header("x-request-id", "req_usage") .body(Body::empty()) .unwrap(), diff --git a/tests/architecture.rs b/tests/architecture.rs index 26282d0fc..8400f2988 100644 --- a/tests/architecture.rs +++ b/tests/architecture.rs @@ -2,7 +2,7 @@ mod architecture { mod accounts_boundary; mod admin_accounts_split; mod admin_boundary; - mod codex_operations_boundary; + mod codex_activity_boundary; mod gateway_boundary; mod platform_boundary; mod runtime_tasks_boundary; diff --git a/tests/architecture/accounts_boundary.rs b/tests/architecture/accounts_boundary.rs index 7345b1d32..9ada0647b 100644 --- a/tests/architecture/accounts_boundary.rs +++ b/tests/architecture/accounts_boundary.rs @@ -1,4 +1,4 @@ -use codex_proxy_rs::codex::accounts::{cookies::jar::CookieJar, models::catalog::ModelCatalog}; +use codex_proxy_rs::codex::{accounts::cookies::jar::CookieJar, models::catalog::ModelCatalog}; #[test] fn accounts_exports_account_scoped_assets() { diff --git a/tests/architecture/admin_accounts_split.rs b/tests/architecture/admin_accounts_split.rs index ece74a11a..18adfeb97 100644 --- a/tests/architecture/admin_accounts_split.rs +++ b/tests/architecture/admin_accounts_split.rs @@ -1,13 +1,15 @@ -use codex_proxy_rs::admin::http::accounts::{ +use codex_proxy_rs::admin::api::accounts::{ cookies::{AccountCookiesData, SetAccountCookiesRequest}, - create::{AccountImportData, CreateAccountRequest, ImportCliAuthRequest}, + create::CreateAccountRequest, delete::{BatchDeleteAccountsRequest, DeleteAccountData}, + export::AccountExportQuery, health::{HealthCheckData, HealthCheckRequest}, - list::{AccountExportQuery, AccountsQuery}, - mutate::{ + import::{AccountImportData, ImportCliAuthRequest}, + lifecycle::{ BatchUpdateAccountStatusRequest, ResetAccountUsageData, UpdateAccountLabelRequest, UpdateAccountStatusRequest, }, + list::AccountsQuery, oauth::{ auth_callback, auth_code_relay, auth_device_login, auth_device_poll, auth_login_start, AdminAuthCallbackQuery, AdminAuthCodeRelayData, AdminAuthCodeRelayRequest, diff --git a/tests/architecture/admin_boundary.rs b/tests/architecture/admin_boundary.rs index cac4abff5..f30f7ee74 100644 --- a/tests/architecture/admin_boundary.rs +++ b/tests/architecture/admin_boundary.rs @@ -1,8 +1,6 @@ use codex_proxy_rs::admin::{ - auth::{api_key::ApiKeyService, service::AdminAuthService}, - http::router::router, - settings::SettingsService, - tasks::session_cleanup::SessionCleanupScheduler, + api::router::router, client_keys::service::ApiKeyService, session::service::AdminAuthService, + settings::SettingsService, tasks::session_cleanup::SessionCleanupScheduler, }; #[test] diff --git a/tests/architecture/codex_operations_boundary.rs b/tests/architecture/codex_activity_boundary.rs similarity index 68% rename from tests/architecture/codex_operations_boundary.rs rename to tests/architecture/codex_activity_boundary.rs index 315d45402..86105e590 100644 --- a/tests/architecture/codex_operations_boundary.rs +++ b/tests/architecture/codex_activity_boundary.rs @@ -1,11 +1,14 @@ use codex_proxy_rs::codex::{ - logs::{event::EventLog, repository::EventLogRepository, service::LogService}, - tasks::{model::ModelRefresher, quota::QuotaRefresher, refresh::RefreshScheduler}, + events::{event::EventLog, repository::EventLogRepository, service::LogService}, + tasks::{ + model_refresh::ModelRefresher, quota_refresh::QuotaRefresher, + token_refresh::RefreshScheduler, + }, usage::service::UsageService, }; #[test] -fn codex_exports_operations_modules() { +fn codex_exports_activity_modules() { let _event_type = std::any::type_name::<EventLog>(); let _event_repo_type = std::any::type_name::<EventLogRepository>(); let _log_service_type = std::any::type_name::<LogService>(); diff --git a/tests/architecture/gateway_boundary.rs b/tests/architecture/gateway_boundary.rs index 84f768c42..efb4f33f1 100644 --- a/tests/architecture/gateway_boundary.rs +++ b/tests/architecture/gateway_boundary.rs @@ -1,6 +1,6 @@ use codex_proxy_rs::codex::gateway::{ fingerprint::model::Fingerprint, oauth::TokenPair, protocol::schema::ResponseFormat, - transport::client::CodexBackendClient, + transport::http_client::CodexBackendClient, }; #[test] diff --git a/tests/architecture/platform_boundary.rs b/tests/architecture/platform_boundary.rs index 151dde5f5..ce7fca743 100644 --- a/tests/architecture/platform_boundary.rs +++ b/tests/architecture/platform_boundary.rs @@ -1,7 +1,7 @@ use codex_proxy_rs::platform::{ crypto::SecretBox, http::auth::{admin_session_id, client_api_key}, - identity::{admin_session::hash_admin_password, api_key::ApiKeyHasher}, + identity::{admin_session::hash_admin_password, client_key::ApiKeyHasher}, storage::db::connect_sqlite, }; diff --git a/tests/codex_accounts.rs b/tests/codex_accounts.rs index 47424b2ee..e93833c16 100644 --- a/tests/codex_accounts.rs +++ b/tests/codex_accounts.rs @@ -2,7 +2,6 @@ mod support; mod codex_accounts { mod cookie_store; - mod model_catalog; mod pool_scheduling; mod refresh; mod refresh_scheduler; diff --git a/tests/codex_events.rs b/tests/codex_events.rs new file mode 100644 index 000000000..f4dcf6c3f --- /dev/null +++ b/tests/codex_events.rs @@ -0,0 +1,3 @@ +mod codex_events { + mod logs_pagination; +} diff --git a/tests/codex_operations/logs_pagination.rs b/tests/codex_events/logs_pagination.rs similarity index 99% rename from tests/codex_operations/logs_pagination.rs rename to tests/codex_events/logs_pagination.rs index c5e1eac97..0baff98e6 100644 --- a/tests/codex_operations/logs_pagination.rs +++ b/tests/codex_events/logs_pagination.rs @@ -1,5 +1,5 @@ use codex_proxy_rs::{ - codex::logs::{ + codex::events::{ event::{EventLevel, EventLog}, repository::EventLogRepository, service::LogService, diff --git a/tests/codex_gateway.rs b/tests/codex_gateway.rs index 7e15c98ad..bc3addc98 100644 --- a/tests/codex_gateway.rs +++ b/tests/codex_gateway.rs @@ -1,9 +1,9 @@ mod codex_gateway { mod cli_auth_import; - mod client; mod fingerprint_update; mod headers; + mod http_client; mod oauth_refresh; - mod sse_usage; + mod usage_events; mod websocket; } diff --git a/tests/codex_gateway/fingerprint_update.rs b/tests/codex_gateway/fingerprint_update.rs index 8e0b69ffb..52e22469e 100644 --- a/tests/codex_gateway/fingerprint_update.rs +++ b/tests/codex_gateway/fingerprint_update.rs @@ -3,7 +3,7 @@ use codex_proxy_rs::{ repository::FingerprintRepository, updater::{FingerprintUpdater, CODEX_DESKTOP_UPDATE_SOURCE}, }, - codex::gateway::transport::client::build_reqwest_client, + codex::gateway::transport::http_client::build_reqwest_client, platform::storage::db::connect_sqlite, }; use wiremock::{ diff --git a/tests/codex_gateway/headers.rs b/tests/codex_gateway/headers.rs index 22fed070e..63e8e1122 100644 --- a/tests/codex_gateway/headers.rs +++ b/tests/codex_gateway/headers.rs @@ -1,8 +1,8 @@ use codex_proxy_rs::{ codex::gateway::fingerprint::{model::Fingerprint, updater::parse_update_manifest}, codex::gateway::transport::{ - client::{build_reqwest_client, CodexBackendClient, CodexRequestContext}, headers::build_codex_headers, + http_client::{build_reqwest_client, CodexBackendClient, CodexRequestContext}, types::CodexResponsesRequest, }, }; diff --git a/tests/codex_gateway/client.rs b/tests/codex_gateway/http_client.rs similarity index 96% rename from tests/codex_gateway/client.rs rename to tests/codex_gateway/http_client.rs index 97e4d197b..c7bc41c55 100644 --- a/tests/codex_gateway/client.rs +++ b/tests/codex_gateway/http_client.rs @@ -7,9 +7,9 @@ use wiremock::{ use codex_proxy_rs::{ codex::gateway::fingerprint::model::Fingerprint, codex::gateway::transport::{ - client::{build_reqwest_client, CodexBackendClient, CodexRequestContext}, + http_client::{build_reqwest_client, CodexBackendClient, CodexRequestContext}, types::CodexResponsesRequest, - usage::TokenUsage, + usage_events::TokenUsage, }, }; diff --git a/tests/codex_gateway/oauth_refresh.rs b/tests/codex_gateway/oauth_refresh.rs index 5d1142cb6..47d3fdec3 100644 --- a/tests/codex_gateway/oauth_refresh.rs +++ b/tests/codex_gateway/oauth_refresh.rs @@ -84,7 +84,7 @@ async fn openai_oauth_client_should_exchange_authorization_code_with_pkce_verifi .exchange_code( "oauth-code", "pkce-verifier", - "http://localhost:1455/auth/callback", + "http://localhost:1455/auth/openai/callback", ) .await .unwrap(); @@ -96,7 +96,7 @@ async fn openai_oauth_client_should_exchange_authorization_code_with_pkce_verifi assert!(body.contains("grant_type=authorization_code")); assert!(body.contains("client_id=codex-client")); assert!(body.contains("code=oauth-code")); - assert!(body.contains("redirect_uri=http%3A%2F%2Flocalhost%3A1455%2Fauth%2Fcallback")); + assert!(body.contains("redirect_uri=http%3A%2F%2Flocalhost%3A1455%2Fauth%2Fopenai%2Fcallback")); assert!(body.contains("code_verifier=pkce-verifier")); } diff --git a/tests/codex_gateway/sse_usage.rs b/tests/codex_gateway/usage_events.rs similarity index 98% rename from tests/codex_gateway/sse_usage.rs rename to tests/codex_gateway/usage_events.rs index 2b2b71e4a..e8c590429 100644 --- a/tests/codex_gateway/sse_usage.rs +++ b/tests/codex_gateway/usage_events.rs @@ -2,7 +2,7 @@ use serde_json::json; use codex_proxy_rs::codex::gateway::transport::{ sse::{encode_sse_event, parse_sse_events}, - usage::{extract_usage, TokenUsage}, + usage_events::{extract_usage, TokenUsage}, }; #[test] diff --git a/tests/codex_gateway/websocket.rs b/tests/codex_gateway/websocket.rs index f76de1ea0..4bd104e87 100644 --- a/tests/codex_gateway/websocket.rs +++ b/tests/codex_gateway/websocket.rs @@ -17,7 +17,9 @@ use tokio_tungstenite::{ }; use codex_proxy_rs::codex::gateway::transport::{ - client::{build_reqwest_client, CodexBackendClient, CodexClientError, CodexRequestContext}, + http_client::{ + build_reqwest_client, CodexBackendClient, CodexClientError, CodexRequestContext, + }, types::CodexResponsesRequest, websocket::{ http_sse_fallback_allowed, transport_for_request, CodexTransport, CodexWebSocketPool, diff --git a/tests/codex_models.rs b/tests/codex_models.rs new file mode 100644 index 000000000..9faed2216 --- /dev/null +++ b/tests/codex_models.rs @@ -0,0 +1,3 @@ +mod codex_models { + mod catalog; +} diff --git a/tests/codex_accounts/model_catalog.rs b/tests/codex_models/catalog.rs similarity index 99% rename from tests/codex_accounts/model_catalog.rs rename to tests/codex_models/catalog.rs index 9cec247bc..b070595ab 100644 --- a/tests/codex_accounts/model_catalog.rs +++ b/tests/codex_models/catalog.rs @@ -1,7 +1,7 @@ use std::collections::BTreeMap; use codex_proxy_rs::{ - codex::accounts::models::{ + codex::models::{ catalog::{ BackendModelEntry, BackendReasoningEffort, BackendTruncationPolicy, ModelCatalog, ModelPlanSnapshot, ParsedModelName, diff --git a/tests/codex_operations.rs b/tests/codex_operations.rs deleted file mode 100644 index 8ff7ff35e..000000000 --- a/tests/codex_operations.rs +++ /dev/null @@ -1,4 +0,0 @@ -mod codex_operations { - mod log_rotation; - mod logs_pagination; -} diff --git a/tests/codex_serving/chat_completions.rs b/tests/codex_serving/chat_completions.rs index 5b6465847..73aa8d1be 100644 --- a/tests/codex_serving/chat_completions.rs +++ b/tests/codex_serving/chat_completions.rs @@ -18,7 +18,7 @@ use codex_proxy_rs::{ UsageStatsConfig, }, platform::crypto::SecretBox, - platform::identity::{api_key::ApiKeyHasher, api_key_repository::ClientApiKeyRepository}, + platform::identity::{client_key::ApiKeyHasher, client_key_repository::ClientApiKeyRepository}, platform::storage::db::connect_sqlite, runtime::build_router, runtime::state::AppState, @@ -159,7 +159,7 @@ async fn build_imported_app_with_accounts( .oneshot( Request::builder() .method("POST") - .uri("/admin/accounts/import") + .uri("/api/admin/accounts/import") .header("content-type", "application/json") .header("cookie", "cpr_admin_session=session_1") .body(Body::from(json!({ "accounts": accounts }).to_string())) diff --git a/tests/codex_serving/diagnostics_route.rs b/tests/codex_serving/diagnostics_route.rs index 0788941af..5157f917d 100644 --- a/tests/codex_serving/diagnostics_route.rs +++ b/tests/codex_serving/diagnostics_route.rs @@ -209,7 +209,7 @@ async fn admin_diagnostics_should_require_admin_session_cookie() { .oneshot( Request::builder() .method("GET") - .uri("/admin/diagnostics") + .uri("/api/admin/diagnostics") .header("x-request-id", "req_admin_diagnostics") .body(Body::empty()) .unwrap(), @@ -233,7 +233,7 @@ async fn admin_diagnostics_should_return_admin_enveloped_runtime_summary_without .oneshot( Request::builder() .method("GET") - .uri("/admin/diagnostics") + .uri("/api/admin/diagnostics") .header("cookie", "cpr_admin_session=session_1") .header("x-request-id", "req_admin_diagnostics") .body(Body::empty()) diff --git a/tests/codex_serving/responses_http_sse.rs b/tests/codex_serving/responses_http_sse.rs index 6d8eee6aa..b9665afe4 100644 --- a/tests/codex_serving/responses_http_sse.rs +++ b/tests/codex_serving/responses_http_sse.rs @@ -12,7 +12,7 @@ use wiremock::{ use codex_proxy_rs::{ codex::accounts::cookies::repository::CookieRepository, - codex::gateway::identity::build_conversation_identity, + codex::gateway::conversation_identity::build_conversation_identity, }; use crate::support::{ diff --git a/tests/codex_serving/routes_responses.rs b/tests/codex_serving/routes_responses.rs index 08a3ab709..5d31d692a 100644 --- a/tests/codex_serving/routes_responses.rs +++ b/tests/codex_serving/routes_responses.rs @@ -9,7 +9,7 @@ use serde_json::{json, Value}; use tower::ServiceExt; use codex_proxy_rs::{ - codex::accounts::models::{ + codex::models::{ catalog::{BackendModelEntry, ModelPlanSnapshot}, repository::ModelSnapshotRepository, }, @@ -19,7 +19,7 @@ use codex_proxy_rs::{ UsageStatsConfig, }, platform::crypto::SecretBox, - platform::identity::{api_key::ApiKeyHasher, api_key_repository::ClientApiKeyRepository}, + platform::identity::{client_key::ApiKeyHasher, client_key_repository::ClientApiKeyRepository}, platform::storage::db::connect_sqlite, runtime::build_router, runtime::state::AppState, diff --git a/tests/platform.rs b/tests/platform.rs index e647cd653..f1233b736 100644 --- a/tests/platform.rs +++ b/tests/platform.rs @@ -1,6 +1,7 @@ mod platform { - mod api_key_auth; + mod client_key_auth; mod crypto; mod http_auth; + mod log_rotation; mod storage_schema; } diff --git a/tests/platform/api_key_auth.rs b/tests/platform/client_key_auth.rs similarity index 88% rename from tests/platform/api_key_auth.rs rename to tests/platform/client_key_auth.rs index 916ee40f6..810e09f7c 100644 --- a/tests/platform/api_key_auth.rs +++ b/tests/platform/client_key_auth.rs @@ -1,4 +1,4 @@ -use codex_proxy_rs::platform::identity::api_key::ApiKeyHasher; +use codex_proxy_rs::platform::identity::client_key::ApiKeyHasher; #[test] fn client_api_key_has_proxy_prefix_and_verifies_against_hash() { diff --git a/tests/codex_operations/log_rotation.rs b/tests/platform/log_rotation.rs similarity index 90% rename from tests/codex_operations/log_rotation.rs rename to tests/platform/log_rotation.rs index 66cd7d280..ed3c9f604 100644 --- a/tests/codex_operations/log_rotation.rs +++ b/tests/platform/log_rotation.rs @@ -1,6 +1,6 @@ use std::{fs, io::Write}; -use codex_proxy_rs::codex::logs::rotation::{build_file_appender, RotationConfig}; +use codex_proxy_rs::platform::logging::rotation::{build_file_appender, RotationConfig}; #[test] fn rolling_appender_should_write_daily_codex_log_file() { diff --git a/tests/runtime/http_trace.rs b/tests/runtime/http_trace.rs index a5a19ecdb..efd59a5db 100644 --- a/tests/runtime/http_trace.rs +++ b/tests/runtime/http_trace.rs @@ -13,7 +13,7 @@ use axum::{ use tower::ServiceExt; use tracing_subscriber::fmt::MakeWriter; -use codex_proxy_rs::{platform::http::middleware::attach_request_id, runtime::http_trace_layer}; +use codex_proxy_rs::{platform::http::request_id::attach_request_id, runtime::http_trace_layer}; #[derive(Clone, Default)] struct SharedLogBuffer { diff --git a/tests/support/admin_accounts.rs b/tests/support/admin_accounts.rs index a1a875fe5..3c9974cd0 100644 --- a/tests/support/admin_accounts.rs +++ b/tests/support/admin_accounts.rs @@ -22,7 +22,7 @@ use codex_proxy_rs::{ UsageStatsConfig, }, platform::crypto::SecretBox, - platform::identity::api_key::ApiKeyHasher, + platform::identity::client_key::ApiKeyHasher, platform::storage::db::connect_sqlite, runtime::build_router, runtime::state::AppState, @@ -255,7 +255,7 @@ pub async fn post_admin_account(app: &Router, payload: Value) -> axum::response: .oneshot( Request::builder() .method("POST") - .uri("/admin/accounts") + .uri("/api/admin/accounts") .header("content-type", "application/json") .header("cookie", "cpr_admin_session=session_1") .body(Body::from(payload.to_string())) @@ -282,7 +282,7 @@ pub async fn import_test_account(app: &Router, session_id: &str, account_id: &st .oneshot( Request::builder() .method("POST") - .uri("/admin/accounts/import") + .uri("/api/admin/accounts/import") .header("content-type", "application/json") .header("cookie", format!("cpr_admin_session={session_id}")) .body(Body::from(import_body.to_string())) diff --git a/tests/support/upstream.rs b/tests/support/upstream.rs index ea10b25b0..94ea9b693 100644 --- a/tests/support/upstream.rs +++ b/tests/support/upstream.rs @@ -16,7 +16,7 @@ use codex_proxy_rs::{ UsageStatsConfig, }, platform::crypto::SecretBox, - platform::identity::{api_key::ApiKeyHasher, api_key_repository::ClientApiKeyRepository}, + platform::identity::{client_key::ApiKeyHasher, client_key_repository::ClientApiKeyRepository}, platform::storage::db::connect_sqlite, runtime::build_router, runtime::state::AppState, @@ -178,7 +178,7 @@ pub async fn build_imported_app_with_accounts( .oneshot( Request::builder() .method("POST") - .uri("/admin/accounts/import") + .uri("/api/admin/accounts/import") .header("content-type", "application/json") .header("cookie", "cpr_admin_session=session_1") .body(Body::from(json!({ "accounts": accounts }).to_string())) @@ -279,7 +279,7 @@ where .oneshot( Request::builder() .method("POST") - .uri("/admin/accounts/import") + .uri("/api/admin/accounts/import") .header("content-type", "application/json") .header("cookie", "cpr_admin_session=session_1") .body(Body::from(json!({ "accounts": accounts }).to_string())) @@ -324,7 +324,7 @@ pub async fn enable_runtime_logging(app: &axum::Router) { .oneshot( Request::builder() .method("PATCH") - .uri("/admin/logs/state") + .uri("/api/admin/logs/state") .header("content-type", "application/json") .header("cookie", "cpr_admin_session=session_1") .body(Body::from(json!({ "enabled": true }).to_string()))