ds-free-api / src /server /admin.rs
heyeyydss's picture
Upload 154 files
69f686e verified
Raw
History Blame
13.2 kB
//! ็ฎก็† API ่ทฏ็”ฑๅค„็†ๅ™จ โ€”โ€” ็™ปๅฝ•/่ฎพ็ฝฎๅฏ†็ ใ€่ดฆๅทๆฑ ็Šถๆ€ใ€่ฏทๆฑ‚็ปŸ่ฎกใ€ๆจกๅž‹ๅˆ—่กจใ€้…็ฝฎๆŸฅ็œ‹
use axum::{
body::Body,
extract::{Query, State},
http::{StatusCode, header},
response::Response,
};
use serde::{Deserialize, Serialize};
use super::handlers::AppState;
use crate::config::Config;
// โ”€โ”€ ่ฏทๆฑ‚/ๅ“ๅบ”็ฑปๅž‹ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
#[derive(Deserialize)]
pub struct SetupRequest {
pub password: String,
}
#[derive(Deserialize)]
pub struct LoginRequest {
pub password: String,
}
#[derive(Serialize)]
pub struct LoginResponse {
pub token: String,
}
#[derive(Serialize)]
pub struct AdminStatusResponse {
pub accounts: Vec<ds_core::AccountStatus>,
pub total: usize,
pub idle: usize,
pub busy: usize,
pub error: usize,
pub invalid: usize,
}
#[derive(Serialize)]
pub struct AdminStatsResponse {
#[serde(flatten)]
pub stats: super::stats::StatsSnapshot,
}
#[derive(Serialize)]
pub struct AdminConfigResponse {
pub server: ServerConfigView,
pub ds_core: DsCoreView,
pub proxy: ProxyConfigView,
pub admin: AdminConfigView,
pub api_keys: Vec<ApiKeyEntryView>,
}
#[derive(Serialize)]
pub struct DsCoreView {
pub accounts: Vec<AccountView>,
pub api_base: String,
pub wasm_url: String,
pub user_agent: String,
pub client_version: String,
pub client_platform: String,
pub client_locale: String,
pub model_types: Vec<String>,
pub max_input_tokens: Vec<u32>,
pub max_output_tokens: Vec<u32>,
pub input_character_limits: Vec<u32>,
pub model_aliases: Vec<String>,
pub tool_call: ToolCallTagConfigView,
}
#[derive(Serialize)]
pub struct ServerConfigView {
pub host: String,
pub port: u16,
pub cors_origins: Vec<String>,
}
#[derive(Serialize)]
pub struct ToolCallTagConfigView {
pub extra_starts: Vec<String>,
pub extra_ends: Vec<String>,
}
#[derive(Serialize)]
pub struct ProxyConfigView {
pub url: Option<String>,
}
#[derive(Serialize)]
pub struct AdminConfigView {
pub password_set: bool,
pub jwt_issued_at: u64,
}
#[derive(Serialize)]
pub struct ApiKeyEntryView {
pub key: String,
pub description: String,
}
#[derive(Serialize)]
pub struct AccountView {
pub email: String,
pub mobile: String,
pub area_code: String,
pub password: String,
}
// โ”€โ”€ ่„ฑๆ• โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
fn mask_config(config: &Config) -> AdminConfigResponse {
AdminConfigResponse {
server: ServerConfigView {
host: config.server.host.clone(),
port: config.server.port,
cors_origins: config.server.cors_origins.clone(),
},
ds_core: DsCoreView {
accounts: config
.ds_core
.accounts
.iter()
.map(|a| AccountView {
email: a.email.clone(),
mobile: a.mobile.clone(),
area_code: a.area_code.clone(),
password: a.password.clone(),
})
.collect(),
api_base: config.ds_core.api_base.clone(),
wasm_url: config.ds_core.wasm_url.clone(),
user_agent: config.ds_core.user_agent.clone(),
client_version: config.ds_core.client_version.clone(),
client_platform: config.ds_core.client_platform.clone(),
client_locale: config.ds_core.client_locale.clone(),
model_types: config.ds_core.model_types.clone(),
max_input_tokens: config.ds_core.max_input_tokens.clone(),
max_output_tokens: config.ds_core.max_output_tokens.clone(),
input_character_limits: config.ds_core.input_character_limits.clone(),
model_aliases: config.ds_core.model_aliases.clone(),
tool_call: ToolCallTagConfigView {
extra_starts: config.ds_core.tool_call.extra_starts.clone(),
extra_ends: config.ds_core.tool_call.extra_ends.clone(),
},
},
proxy: ProxyConfigView {
url: config.proxy.url.clone(),
},
admin: AdminConfigView {
password_set: !config.admin.password_hash.is_empty(),
jwt_issued_at: config.admin.jwt_issued_at,
},
api_keys: config
.api_keys
.iter()
.map(|k| ApiKeyEntryView {
key: k.key.clone(),
description: k.description.clone(),
})
.collect(),
}
}
// โ”€โ”€ Handlers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
/// POST /admin/api/setup โ€” ้ฆ–ๆฌก่ฎพ็ฝฎๅฏ†็ 
pub(crate) async fn admin_setup(
State(state): State<AppState>,
body: axum::body::Bytes,
) -> Response {
let req: SetupRequest = match serde_json::from_slice(&body) {
Ok(r) => r,
Err(e) => return error_response(StatusCode::BAD_REQUEST, &format!("่ฏทๆฑ‚ๆ ผๅผ้”™่ฏฏ: {}", e)),
};
match super::auth::setup_admin(&state.store, &state.login_limiter, &req.password).await {
Ok(token) => json_response(&LoginResponse { token }),
Err(msg) => {
let status = if msg.contains("ๅทฒ่ฎพ็ฝฎ") {
StatusCode::FORBIDDEN
} else if msg.contains("ๆฌกๆ•ฐ่ฟ‡ๅคš") {
StatusCode::TOO_MANY_REQUESTS
} else if msg.contains("่‡ณๅฐ‘ 6 ไฝ") {
StatusCode::BAD_REQUEST
} else {
StatusCode::INTERNAL_SERVER_ERROR
};
error_response(status, &msg)
}
}
}
pub(crate) async fn admin_login(
State(state): State<AppState>,
body: axum::body::Bytes,
) -> Response {
let req: LoginRequest = match serde_json::from_slice(&body) {
Ok(r) => r,
Err(e) => return error_response(StatusCode::BAD_REQUEST, &format!("่ฏทๆฑ‚ๆ ผๅผ้”™่ฏฏ: {}", e)),
};
match super::auth::login_admin(&state.store, &state.login_limiter, &req.password).await {
Ok(token) => json_response(&LoginResponse { token }),
Err(msg) => {
let status = if msg.contains("ๆฌกๆ•ฐ่ฟ‡ๅคš") {
StatusCode::TOO_MANY_REQUESTS
} else if msg.contains("ๆœช่ฎพ็ฝฎๅฏ†็ ") {
StatusCode::FORBIDDEN
} else {
StatusCode::UNAUTHORIZED
};
error_response(status, &msg)
}
}
}
/// GET /admin/api/status
pub(crate) async fn admin_status(State(state): State<AppState>) -> Response {
let statuses = state.adapter.account_statuses();
let total = statuses.len();
let busy = statuses.iter().filter(|a| a.state == "busy").count();
let idle = statuses.iter().filter(|a| a.state == "idle").count();
let error = statuses.iter().filter(|a| a.state == "error").count();
let invalid = statuses.iter().filter(|a| a.state == "invalid").count();
let resp = AdminStatusResponse {
accounts: statuses,
total,
idle,
busy,
error,
invalid,
};
json_response(&resp)
}
/// GET /admin/api/stats
pub(crate) async fn admin_stats(State(state): State<AppState>) -> Response {
let snapshot = state.stats.snapshot();
let resp = AdminStatsResponse { stats: snapshot };
json_response(&resp)
}
/// GET /admin/api/models
pub(crate) async fn admin_models(State(state): State<AppState>) -> Response {
let models = state.adapter.list_models().await;
json_response(&models)
}
/// GET /admin/api/config
pub(crate) async fn admin_config(State(state): State<AppState>) -> Response {
let config = state.config.read().await;
let config_view = mask_config(&config);
json_response(&config_view)
}
/// PUT /admin/api/config โ€” ๆ›ดๆ–ฐๅนถ็ƒญ้‡่ฝฝ้…็ฝฎ
pub(crate) async fn admin_put_config(
State(state): State<AppState>,
body: axum::body::Bytes,
) -> Response {
let mut new_config: Config = match serde_json::from_slice(&body) {
Ok(c) => c,
Err(e) => return error_response(StatusCode::BAD_REQUEST, &format!("JSON ่งฃๆžๅคฑ่ดฅ: {}", e)),
};
// Validate
if let Err(e) = new_config.validate() {
return error_response(StatusCode::BAD_REQUEST, &e.to_string());
}
// Merge: empty/"***" passwords keep existing values from current config;
// API keys match by `id` (stable identifier), falling back to description for old-format migration.
{
let current = state.config.read().await;
for a in &mut new_config.ds_core.accounts {
if (a.password.is_empty() || a.password == "***")
&& let Some(existing) = current
.ds_core
.accounts
.iter()
.find(|e| e.email == a.email && e.mobile == a.mobile)
{
a.password.clone_from(&existing.password);
}
}
// Admin ้…็ฝฎ๏ผš็ฉบ็š„ password_hash/jwt_secret ไฟ็•™็Žฐๆœ‰ๅ€ผ๏ผˆๅ‰็ซฏไธ่ฟ”ๅ›ž่ฟ™ไบ›ๅญ—ๆฎต๏ผ‰
if new_config.admin.password_hash.is_empty() {
new_config
.admin
.password_hash
.clone_from(&current.admin.password_hash);
}
if new_config.admin.jwt_secret.is_empty() {
new_config
.admin
.jwt_secret
.clone_from(&current.admin.jwt_secret);
}
// ๅฏ†็ ไฟฎๆ”น๏ผšๅ‰็ซฏๅ‘ old_password + new_password
if !new_config.admin.old_password.is_empty() || !new_config.admin.new_password.is_empty() {
if new_config.admin.old_password.is_empty() || new_config.admin.new_password.is_empty()
{
return error_response(
StatusCode::BAD_REQUEST,
"ไฟฎๆ”นๅฏ†็ ้œ€่ฆๅŒๆ—ถๆไพ›ๆ—งๅฏ†็ ๅ’Œๆ–ฐๅฏ†็ ",
);
}
if !bcrypt::verify(&new_config.admin.old_password, &current.admin.password_hash)
.unwrap_or(false)
{
return error_response(StatusCode::BAD_REQUEST, "ๆ—งๅฏ†็ ไธๆญฃ็กฎ");
}
new_config.admin.password_hash =
super::store::hash_password(&new_config.admin.new_password);
new_config.admin.jwt_secret = super::store::generate_hex_secret();
new_config.admin.jwt_issued_at += 1;
}
}
// Persist
{
let mut guard = state.config.write().await;
*guard = new_config.clone();
if let Err(e) = guard.save(&state.config_path) {
return error_response(
StatusCode::INTERNAL_SERVER_ERROR,
&format!("ไฟๅญ˜ๅคฑ่ดฅ: {}", e),
);
}
}
// Hot-reload: sync accounts from the new config
state
.adapter
.sync_accounts(&new_config.ds_core.accounts)
.await;
json_response(&serde_json::json!({"ok": true}))
}
#[derive(Deserialize)]
pub struct LogsQuery {
#[serde(default = "default_limit")]
pub limit: usize,
}
fn default_limit() -> usize {
50
}
/// GET /admin/api/logs โ€” ่Žทๅ–ๆœ€่ฟ‘็š„่ฏทๆฑ‚ๆ—ฅๅฟ—
pub(crate) async fn admin_logs(
Query(query): Query<LogsQuery>,
State(state): State<AppState>,
) -> Response {
let logs = state.stats.recent_logs(query.limit);
json_response(&logs)
}
#[derive(Deserialize)]
pub struct RuntimeLogsQuery {
#[serde(default)]
pub offset: usize,
#[serde(default = "default_runtime_limit")]
pub limit: usize,
}
fn default_runtime_limit() -> usize {
100
}
/// GET /admin/api/runtime-logs โ€” ๅˆ†้กตๆŸฅ่ฏข่ฟ่กŒๆ—ฅๅฟ—
pub(crate) async fn admin_runtime_logs(Query(query): Query<RuntimeLogsQuery>) -> Response {
let (total, logs) = super::runtime_log::query_logs(query.offset, query.limit).await;
json_response(&serde_json::json!({
"total": total,
"offset": query.offset,
"limit": query.limit,
"logs": logs,
}))
}
// โ”€โ”€ Helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
fn json_response<T: Serialize>(data: &T) -> Response {
let bytes = serde_json::to_vec(data).unwrap();
Response::builder()
.status(StatusCode::OK)
.header(header::CONTENT_TYPE, "application/json")
.body(Body::from(bytes))
.unwrap()
}
fn error_response(status: StatusCode, message: &str) -> Response {
let body = serde_json::json!({"error": message});
let bytes = serde_json::to_vec(&body).unwrap();
Response::builder()
.status(status)
.header(header::CONTENT_TYPE, "application/json")
.body(Body::from(bytes))
.unwrap()
}