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;
/// 会话清理调度器 - 定期删除过期的管理员会话