Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions src-tauri/src/commands/user_token.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use serde::{Deserialize, Serialize};
use crate::modules::user_token_db::{self, UserToken, TokenIpBinding};
use crate::modules::security_db;
use crate::modules::token_stats;

#[derive(Debug, Serialize, Deserialize)]
pub struct CreateTokenRequest {
Expand All @@ -10,6 +12,7 @@ pub struct CreateTokenRequest {
pub curfew_start: Option<String>,
pub curfew_end: Option<String>,
pub custom_expires_at: Option<i64>, // 自定义过期时间戳 (秒)
pub allowed_models: Vec<String>, // 允许访问的模型列表,空列表表示不限制
}

#[derive(Debug, Serialize, Deserialize)]
Expand All @@ -20,6 +23,7 @@ pub struct UpdateTokenRequest {
pub max_ips: Option<i32>,
pub curfew_start: Option<Option<String>>,
pub curfew_end: Option<Option<String>>,
pub allowed_models: Option<Vec<String>>, // 允许访问的模型列表,None=不更新
}

// 命令实现
Expand All @@ -41,6 +45,7 @@ pub async fn create_user_token(request: CreateTokenRequest) -> Result<UserToken,
request.curfew_start,
request.curfew_end,
request.custom_expires_at,
request.allowed_models,
)
}

Expand All @@ -55,6 +60,7 @@ pub async fn update_user_token(id: String, request: UpdateTokenRequest) -> Resul
request.max_ips,
request.curfew_start,
request.curfew_end,
request.allowed_models,
)
}

Expand Down Expand Up @@ -82,6 +88,7 @@ pub struct UserTokenStats {
pub active_tokens: usize,
pub total_users: usize,
pub today_requests: i64,
pub today_tokens: i64,
}

/// 获取简单的统计信息
Expand All @@ -96,13 +103,17 @@ pub async fn get_user_token_summary() -> Result<UserTokenStats, String> {
users.insert(t.username.clone());
}

// 这里简单返回一些数据,请求数最好从数据库聚合查询
// 目前仅作为演示,请求数暂不精确统计今日的
// 从安全数据库获取今日请求数
let ip_stats = security_db::get_ip_stats()?;

// 从 token_stats 获取今日 tokens (24小时)
let token_summary = token_stats::get_summary_stats(24)?;

Ok(UserTokenStats {
total_tokens: tokens.len(),
active_tokens,
total_users: users.len(),
today_requests: 0, // TODO: Implement daily stats query
today_requests: ip_stats.today_requests as i64,
today_tokens: token_summary.total_tokens as i64,
})
}
48 changes: 42 additions & 6 deletions src-tauri/src/modules/user_token_db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use rusqlite::{params, Connection, OptionalExtension};
use serde::{Deserialize, Serialize};
use serde_json;
use std::path::PathBuf;
use uuid::Uuid;
use chrono::{Utc, Local, Timelike, FixedOffset};
Expand All @@ -28,6 +29,7 @@ pub struct UserToken {
pub last_used_at: Option<i64>,
pub total_requests: i64,
pub total_tokens_used: i64,
pub allowed_models: Vec<String>, // 允许访问的模型列表,空列表表示不限制
}

/// 令牌 IP 绑定结构体
Expand Down Expand Up @@ -105,6 +107,8 @@ pub fn init_db() -> Result<(), String> {
let _ = conn.execute("ALTER TABLE user_tokens ADD COLUMN last_used_at INTEGER", []);
let _ = conn.execute("ALTER TABLE user_tokens ADD COLUMN curfew_start TEXT", []);
let _ = conn.execute("ALTER TABLE user_tokens ADD COLUMN curfew_end TEXT", []);
let _ = conn.execute("ALTER TABLE user_tokens ADD COLUMN allowed_models TEXT", []);
let _ = conn.execute("UPDATE user_tokens SET allowed_models = '[]' WHERE allowed_models IS NULL", []);

// 创建 token_ip_bindings 表
conn.execute(
Expand Down Expand Up @@ -161,7 +165,8 @@ pub fn create_token(
max_ips: i32,
curfew_start: Option<String>,
curfew_end: Option<String>,
custom_expires_at: Option<i64> // 自定义过期时间戳 (秒)
custom_expires_at: Option<i64>,
allowed_models: Vec<String> // 允许访问的模型列表
) -> Result<UserToken, String> {
let conn = connect_db()?;
let id = Uuid::new_v4().to_string();
Expand Down Expand Up @@ -192,14 +197,15 @@ pub fn create_token(
last_used_at: None,
total_requests: 0,
total_tokens_used: 0,
allowed_models,
};

conn.execute(
"INSERT INTO user_tokens (
id, token, username, description, enabled, expires_type, expires_at, max_ips,
curfew_start, curfew_end,
created_at, updated_at, total_requests, total_tokens_used
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14)",
created_at, updated_at, total_requests, total_tokens_used, allowed_models
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15)",
params![
user_token.id,
user_token.token,
Expand All @@ -215,6 +221,7 @@ pub fn create_token(
user_token.updated_at,
user_token.total_requests,
user_token.total_tokens_used,
serde_json::to_string(&user_token.allowed_models).unwrap_or_else(|_| "[]".to_string()),
],
).map_err(|e| format!("Failed to insert user token: {}", e))?;

Expand Down Expand Up @@ -244,6 +251,9 @@ pub fn list_tokens() -> Result<Vec<UserToken>, String> {
last_used_at: row.get("last_used_at").unwrap_or(None),
total_requests: row.get("total_requests").unwrap_or(0),
total_tokens_used: row.get("total_tokens_used").unwrap_or(0),
allowed_models: row.get::<_, Option<String>>("allowed_models")?
.map(|s| serde_json::from_str(&s).unwrap_or_default())
.unwrap_or_default(),
})
}).map_err(|e| format!("Failed to query tokens: {}", e))?;

Expand Down Expand Up @@ -278,6 +288,9 @@ pub fn get_token_by_id(id: &str) -> Result<Option<UserToken>, String> {
last_used_at: row.get("last_used_at")?,
total_requests: row.get("total_requests")?,
total_tokens_used: row.get("total_tokens_used")?,
allowed_models: row.get::<_, Option<String>>("allowed_models")?
.map(|s| serde_json::from_str(&s).unwrap_or_default())
.unwrap_or_default(),
})
}).optional().map_err(|e| format!("Failed to query token: {}", e))?;

Expand Down Expand Up @@ -307,6 +320,9 @@ pub fn get_token_by_value(token: &str) -> Result<Option<UserToken>, String> {
last_used_at: row.get("last_used_at")?,
total_requests: row.get("total_requests")?,
total_tokens_used: row.get("total_tokens_used")?,
allowed_models: row.get::<_, Option<String>>("allowed_models")?
.map(|s| serde_json::from_str(&s).unwrap_or_default())
.unwrap_or_default(),
})
}).optional().map_err(|e| format!("Failed to query token: {}", e))?;

Expand All @@ -321,7 +337,8 @@ pub fn update_token(
enabled: Option<bool>,
max_ips: Option<i32>,
curfew_start: Option<Option<String>>,
curfew_end: Option<Option<String>>
curfew_end: Option<Option<String>>,
allowed_models: Option<Vec<String>>
) -> Result<(), String> {
let conn = connect_db()?;
let now = Utc::now().timestamp();
Expand Down Expand Up @@ -366,6 +383,12 @@ pub fn update_token(
param_idx += 1;
}

if let Some(models) = allowed_models {
query.push_str(&format!(", allowed_models = ?{}", param_idx));
params_vec.push(Box::new(serde_json::to_string(&models).unwrap_or_else(|_| "[]".to_string())));
param_idx += 1;
}

query.push_str(&format!(" WHERE id = ?{}", param_idx));
params_vec.push(Box::new(id.to_string()));

Expand Down Expand Up @@ -498,9 +521,9 @@ pub fn record_token_usage_and_ip(
Ok(())
}

/// 检查 Token 是否有效 (包含过期时间检查和 IP 限制检查)
/// 检查 Token 是否有效 (包含过期时间检查、IP 限制检查和模型访问控制)
/// 返回: (是否有效, 拒绝原因)
pub fn validate_token(token_str: &str, ip: &str) -> Result<(bool, Option<String>), String> {
pub fn validate_token(token_str: &str, ip: &str, model: Option<&str>) -> Result<(bool, Option<String>), String> {
let token_opt = get_token_by_value(token_str)?;

if let Some(token) = token_opt {
Expand Down Expand Up @@ -560,6 +583,19 @@ pub fn validate_token(token_str: &str, ip: &str) -> Result<(bool, Option<String>
}
}

// 4. 检查模型访问限制
if let Some(model_name) = model {
if !token.allowed_models.is_empty() {
if !token.allowed_models.contains(&model_name.to_string()) {
return Ok((false, Some(format!(
"Model '{}' is not allowed for this token. Allowed models: {}",
model_name,
token.allowed_models.join(", ")
))));
}
}
}

// 一切正常,Token 有效
Ok((true, None))
} else {
Expand Down
3 changes: 2 additions & 1 deletion src-tauri/src/proxy/middleware/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,8 @@ async fn auth_middleware_internal(
.unwrap_or_else(|| "127.0.0.1".to_string()); // Default fallback

// 验证 Token
match crate::modules::user_token_db::validate_token(token, &client_ip) {
// 注意:模型验证在后续请求处理器中进行,这里暂时传递 None
match crate::modules::user_token_db::validate_token(token, &client_ip, None) {
Ok((true, _)) => {
// Token 有效,查询信息以便传递
if let Ok(Some(user_token)) = crate::modules::user_token_db::get_token_by_value(token) {
Expand Down
5 changes: 5 additions & 0 deletions src/components/proxy/ProxyMonitor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ interface ProxyRequestLog {
output_tokens?: number;
account_email?: string;
protocol?: string; // "openai" | "anthropic" | "gemini"
username?: string; // User Token username
}

interface ProxyStats {
Expand Down Expand Up @@ -66,6 +67,7 @@ const LogTable: React.FC<LogTableProps> = ({
<th style={{ width: '220px' }}>{t('monitor.table.model')}</th>
<th style={{ width: '70px' }}>{t('monitor.table.protocol')}</th>
<th style={{ width: '140px' }}>{t('monitor.table.account')}</th>
<th style={{ width: '100px' }}>{t('monitor.table.user')}</th>
<th style={{ width: '180px' }}>{t('monitor.table.path')}</th>
<th className="text-right" style={{ width: '90px' }}>{t('monitor.table.usage')}</th>
<th className="text-right" style={{ width: '80px' }}>{t('monitor.table.duration')}</th>
Expand Down Expand Up @@ -105,6 +107,9 @@ const LogTable: React.FC<LogTableProps> = ({
<td className="text-gray-600 dark:text-gray-400 truncate text-[10px]" style={{ width: '140px', maxWidth: '140px' }} title={log.account_email || ''}>
{log.account_email ? log.account_email.replace(/(.{3}).*(@.*)/, '$1***$2') : '-'}
</td>
<td className="text-gray-600 dark:text-gray-400 truncate text-[10px]" style={{ width: '100px', maxWidth: '100px' }} title={log.username || ''}>
{log.username || '-'}
</td>
<td className="truncate" style={{ width: '180px', maxWidth: '180px' }}>{log.url}</td>
<td className="text-right text-[9px]" style={{ width: '90px' }}>
{log.input_tokens != null && <div>I: {formatCompactNumber(log.input_tokens)}</div>}
Expand Down
1 change: 1 addition & 0 deletions src/locales/ar.json
Original file line number Diff line number Diff line change
Expand Up @@ -966,6 +966,7 @@
"model": "النموذج",
"protocol": "البروتوكول",
"account": "الحساب",
"user": "المستخدم",
"path": "المسار",
"usage": "توكنز",
"duration": "المدة",
Expand Down
2 changes: 2 additions & 0 deletions src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1140,6 +1140,7 @@
"model": "Model",
"protocol": "Protocol",
"account": "Account",
"user": "User",
"path": "Path",
"usage": "Tokens",
"duration": "Duration",
Expand Down Expand Up @@ -1364,6 +1365,7 @@
"ip_limit": "IP Limit",
"created": "Created",
"today_requests": "Today Requests",
"today_tokens": "Today Tokens",
"never": "Never",
"renew": "Renew",
"renew_button": "Renew",
Expand Down
1 change: 1 addition & 0 deletions src/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -962,6 +962,7 @@
"model": "Modelo",
"protocol": "Protocolo",
"account": "Cuenta",
"user": "Usuario",
"path": "Ruta",
"usage": "Tokens",
"duration": "Duración",
Expand Down
1 change: 1 addition & 0 deletions src/locales/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -1086,6 +1086,7 @@
"model": "モデル",
"protocol": "プロトコル",
"account": "アカウント",
"user": "ユーザー",
"path": "パス",
"usage": "トークン",
"duration": "所要時間",
Expand Down
1 change: 1 addition & 0 deletions src/locales/ko.json
Original file line number Diff line number Diff line change
Expand Up @@ -996,6 +996,7 @@
"model": "모델",
"protocol": "프로토콜",
"account": "계정",
"user": "사용자",
"path": "경로",
"usage": "토큰",
"duration": "소요 시간",
Expand Down
1 change: 1 addition & 0 deletions src/locales/my.json
Original file line number Diff line number Diff line change
Expand Up @@ -962,6 +962,7 @@
"model": "Model",
"protocol": "Protokol",
"account": "Akaun",
"user": "Pengguna",
"path": "Laluan",
"usage": "Token",
"duration": "Tempoh",
Expand Down
1 change: 1 addition & 0 deletions src/locales/pt.json
Original file line number Diff line number Diff line change
Expand Up @@ -962,6 +962,7 @@
"model": "Modelo",
"protocol": "Protocolo",
"account": "Conta",
"user": "Usuário",
"path": "Caminho",
"usage": "Tokens",
"duration": "Duração",
Expand Down
1 change: 1 addition & 0 deletions src/locales/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -963,6 +963,7 @@
"model": "Модель",
"protocol": "Протокол",
"account": "Аккаунт",
"user": "Пользователь",
"path": "Путь",
"usage": "Токены",
"duration": "Длительность",
Expand Down
1 change: 1 addition & 0 deletions src/locales/tr.json
Original file line number Diff line number Diff line change
Expand Up @@ -938,6 +938,7 @@
"model": "Model",
"protocol": "Protokol",
"account": "Hesap",
"user": "Kullanıcı",
"path": "Yol",
"usage": "Token'lar",
"duration": "Süre",
Expand Down
1 change: 1 addition & 0 deletions src/locales/vi.json
Original file line number Diff line number Diff line change
Expand Up @@ -969,6 +969,7 @@
"model": "Model",
"protocol": "Giao thức",
"account": "Tài khoản",
"user": "Người dùng",
"path": "Đường dẫn (Path)",
"usage": "Tokens",
"duration": "Thời gian",
Expand Down
1 change: 1 addition & 0 deletions src/locales/zh-TW.json
Original file line number Diff line number Diff line change
Expand Up @@ -996,6 +996,7 @@
"model": "模型",
"protocol": "協定",
"account": "帳號",
"user": "用戶",
"path": "路徑",
"usage": "Token 消耗",
"duration": "耗時",
Expand Down
19 changes: 18 additions & 1 deletion src/locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -1133,6 +1133,7 @@
"model": "模型",
"protocol": "协议",
"account": "账号",
"user": "用户",
"path": "路径",
"usage": "Token 消耗",
"duration": "耗时",
Expand Down Expand Up @@ -1329,6 +1330,7 @@
"ip_limit": "IP 限制",
"created": "创建时间",
"today_requests": "今日请求数",
"today_tokens": "今日 Tokens",
"never": "永不过期",
"renew": "续期",
"renew_button": "续费",
Expand All @@ -1348,6 +1350,21 @@
"placeholder_desc": "选填备注",
"placeholder_max_ips": "0 = 不限制",
"hint_max_ips": "0 表示不限制",
"hint_curfew": "留空则禁用。基于服务器时间。"
"hint_curfew": "留空则禁用。基于服务器时间。",
"allowed_models": "允许使用的模型",
"select_all": "全选",
"clear_all": "清空",
"models_unlimited": "留空 = 所有模型可用",
"models_selected": "个模型已选",
"custom_expires_at": "自定义过期时间",
"hint_custom_expires": "选择此 Token 过期的确切日期和时间",
"expires_custom": "自定义",
"copy_failed": "复制失败",
"load_failed": "加载失败",
"custom_expires_required": "请选择自定义过期时间",
"expires_in": "有效期",
"curfew_time_range": "宵禁时间段",
"curfew_to": "至",
"curfew_hint": "留空则禁用宵禁。基于服务器时间。"
}
}
Loading