mirror of
https://github.com/zyycn/codex-proxy-rs.git
synced 2026-11-15 08:06:21 +00:00
refactor: align backend names
Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
@@ -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;
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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)),
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
))
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
))
|
||||
}
|
||||
@@ -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)),
|
||||
}
|
||||
}
|
||||
@@ -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)),
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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)),
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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},
|
||||
};
|
||||
@@ -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},
|
||||
};
|
||||
@@ -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)),
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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,
|
||||
@@ -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;
|
||||
|
||||
/// 会话清理调度器 - 定期删除过期的管理员会话
|
||||
|
||||
Reference in New Issue
Block a user