refactor: align backend names

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
zyycn
2026-06-14 17:48:25 +08:00
co-authored by Codex
parent 48230ae72a
commit f557cb17ba
170 changed files with 2293 additions and 1339 deletions
@@ -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;
+42
View File
@@ -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),
))
}
@@ -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;
@@ -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>,
@@ -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,
};
@@ -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>,
@@ -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,
};
+51
View File
@@ -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),
))
}
@@ -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,
@@ -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,
};
@@ -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,
};
+28
View File
@@ -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)),
}
}
+44
View File
@@ -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),
))
}
+40
View File
@@ -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,
),
))
}
+116
View File
@@ -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)),
}
}
+40
View File
@@ -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)),
}
}
+316
View File
@@ -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,
)
}
@@ -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,
};
+35
View File
@@ -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)),
}
}
+60
View File
@@ -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,
),
}
}
+71
View File
@@ -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())
}
+120
View File
@@ -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,
}
}
}
@@ -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::{
@@ -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,
};
+111
View File
@@ -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),
)
}
@@ -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,
};
@@ -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,
};
@@ -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},
};
+1
View File
@@ -0,0 +1 @@
pub mod service;
@@ -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},
};
-519
View File
@@ -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)),
}
}
-247
View File
@@ -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,
),
}
}
-103
View File
@@ -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),
)
}
+3 -2
View File
@@ -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;
@@ -1,3 +1,2 @@
pub mod api_key;
pub mod repository;
pub mod service;
@@ -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,
+1 -1
View File
@@ -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;
/// 会话清理调度器 - 定期删除过期的管理员会话
+1 -2
View File
@@ -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;
@@ -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)
+9
View File
@@ -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;
@@ -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());
}
}
+1 -1
View File
@@ -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,
},
};
+2 -2
View File
@@ -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 {
@@ -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 {
+2 -2
View File
@@ -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;
+1 -1
View File
@@ -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,
};
@@ -1,4 +1,3 @@
pub mod event;
pub mod repository;
pub mod rotation;
pub mod service;
@@ -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},
};
@@ -3,7 +3,7 @@ use std::sync::Arc;
use tokio::sync::RwLock;
use crate::{
codex::logs::{
codex::events::{
event::EventLog,
repository::{EventLogFilters, EventLogRepository},
},
+2 -2
View File
@@ -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;
+1 -1
View File
@@ -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 {
+2 -2
View File
@@ -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::*;
@@ -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)]
+2 -2
View File
@@ -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;
+2 -1
View File
@@ -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;
@@ -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 {
@@ -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,
};
+3 -3
View File
@@ -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,
+1 -1
View File
@@ -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,
},
},
@@ -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;
+1 -1
View File
@@ -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};
+15 -15
View File
@@ -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(),
@@ -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,
},
};
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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,
};
+2 -2
View File
@@ -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,
};
+1 -1
View File
@@ -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>) {
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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};
+2 -2
View File
@@ -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,
+3 -3
View File
@@ -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;
@@ -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 {
+1
View File
@@ -1,6 +1,7 @@
pub mod admin;
pub mod codex;
pub mod runtime;
pub mod web;
pub mod config;
pub mod platform;
+1 -1
View File
@@ -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;
+3
View File
@@ -0,0 +1,3 @@
pub mod secret_box;
pub use secret_box::{CryptoError, CryptoResult, SecretBox};
+1 -1
View File
@@ -1,3 +1,3 @@
pub mod auth;
pub mod health;
pub mod middleware;
pub mod request_id;
@@ -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},
};
+2 -2
View File
@@ -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;
+1
View File
@@ -0,0 +1 @@
pub mod rotation;
+1
View File
@@ -1,4 +1,5 @@
pub mod crypto;
pub mod http;
pub mod identity;
pub mod logging;
pub mod storage;

Some files were not shown because too many files have changed in this diff Show More