Skip to content
Merged
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
12 changes: 11 additions & 1 deletion i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,17 @@
"InvalidCertificateGeneric": "Login failed: certificate or secure connection issue.",
"NetworkError": "Login failed: network error. Please check connectivity.",
"VersionIncompatible": "The client version is incompatible with JumpServer versions earlier than v4.10.12-lts. Please download and use the v3.0.7 client.",
"VersionNoMatch": "The client version does not match the JumpServer version. Please download and use the v{version} client, or upgrade JumpServer to the latest version."
"VersionNoMatch": "The client version does not match the JumpServer version. Please download and use the v{version} client, or upgrade JumpServer to the latest version.",
"SessionChecking": "Checking client sign-in…",
"SessionCheckFailed": "Unable to verify sign-in. Check your connection and try again.",
"SessionRetry": "Retry",
"LifetimeUntil": "This sign-in expires at {at} ({duration})",
"LifetimeHours": "{hours}h",
"LifetimeMinutes": "{minutes}m",
"LifetimeUnlimited": "No maximum login lifetime is configured",
"LifetimeSoon": "Sign-in expires soon",
"LifetimeSoonDescription": "{site}: {remaining} remaining. Sign-in expires at {at}; please sign in again then.",
"LifetimeCountdown": "{minutes}m {seconds}s"
},
"Asset": {
"GetAssetFailed": "Get asset failed, please refresh and try again"
Expand Down
12 changes: 11 additions & 1 deletion i18n/locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,17 @@
"InvalidCertificateGeneric": "登录失败:证书或安全连接异常。",
"NetworkError": "登录失败:网络异常,请检查连接",
"VersionIncompatible": "客户端与JumpServer 版本(< v4.10.12-lts)不匹配,请下载使用 v3.0.7 的客户端",
"VersionNoMatch": "客户端与 JumpServer 版本不匹配,请下载使用 v{version} 版本的客户端或将 JumpServer 更新至最新"
"VersionNoMatch": "客户端与 JumpServer 版本不匹配,请下载使用 v{version} 版本的客户端或将 JumpServer 更新至最新",
"SessionChecking": "正在验证客户端登录状态…",
"SessionCheckFailed": "暂时无法验证登录状态,请检查网络后重试。",
"SessionRetry": "重试",
"LifetimeUntil": "本次登录有效期至 {at}({duration})",
"LifetimeHours": "{hours} 小时",
"LifetimeMinutes": "{minutes} 分钟",
"LifetimeUnlimited": "未设置最长登录时间",
"LifetimeSoon": "登录即将到期",
"LifetimeSoonDescription": "{site}:剩余 {remaining},将于 {at} 到期,届时需要重新登录。",
"LifetimeCountdown": "{minutes} 分 {seconds} 秒"
},
"Loading": {
"Loading": "加载中",
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "jumpserver-client",
"type": "module",
"version": "4.1.2",
"version": "4.2.0",
"private": true,
"packageManager": "pnpm@11.4.0",
"description": "JumpServer client tool",
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "jumpserver-client"
version = "4.1.2"
version = "4.2.0"
description = "JumpServer client tool"
authors = [ "JumpServer" ]
license = "MIT"
Expand Down
10 changes: 1 addition & 9 deletions src-tauri/src/api/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,13 @@ pub(crate) fn api_client_for_origin(origin: &str) -> Result<Client, reqwest::Err
instance_client_builder_for_origin(origin).build()
}

/// 构建一个用于 OAuth 请求的 HTTP 客户端,该客户端不允许重定向。
///
/// 目前,OAuth 代码交换需要禁用重定向处理,同时仍需保持与普通 API 请求相同的证书行为
pub(crate) fn oauth_client() -> Result<Client, reqwest::Error> {
instance_client_builder()
.redirect(redirect::Policy::none())
.build()
}

/// 构建绑定指定站点 origin 的 OAuth HTTP 客户端。
///
/// OAuth code/token 交换同样需要对本地回环地址绕过代理。
pub(crate) fn oauth_client_for_origin(origin: &str) -> Result<Client, reqwest::Error> {
instance_client_builder_for_origin(origin)
.redirect(redirect::Policy::none())
.timeout(std::time::Duration::from_secs(10))
.build()
}

Expand Down
19 changes: 17 additions & 2 deletions src-tauri/src/api/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,17 +45,32 @@ impl ApiSessionStore {
}

/// 更新当前会话保存的 bearer token
pub fn update_current_bearer_token(&self, bearer_token: String) -> Result<(), String> {
pub fn update_bearer_token(&self, origin: &str, bearer_token: String) -> Result<(), String> {
let mut values = self.values.write().expect("api session lock poisoned");
let session_key = values
.get(CURRENT_SESSION_KEY)
.cloned()
.ok_or_else(|| "missing current api session".to_string())?;

values.insert(Self::field_key(&session_key, "bearer_token"), bearer_token);
if values
.get(&Self::field_key(&session_key, "origin"))
.map(String::as_str)
== Some(origin)
{
values.insert(Self::field_key(&session_key, "bearer_token"), bearer_token);
}
Ok(())
}

pub fn clear_site(&self, site: &str) {
let mut values = self.values.write().expect("api session lock poisoned");
let prefix = format!("session:{}:", site);
values.retain(|key, _| !key.starts_with(&prefix));
if values.get(CURRENT_SESSION_KEY).map(String::as_str) == Some(site) {
values.remove(CURRENT_SESSION_KEY);
}
}

/// 读取当前会话上下文,缺少任一关键字段时返回 None
pub fn current_context(&self) -> Option<ApiSessionContext> {
let values = self.values.read().expect("api session lock poisoned");
Expand Down
61 changes: 51 additions & 10 deletions src-tauri/src/commands/api_session.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
use crate::{
api::session::{ApiSessionContext, ApiSessionStore},
service::oauth::ensure_fresh_token,
service::client_session,
};
use tauri::{AppHandle, State};
use tauri::{AppHandle, Emitter, State};

#[tauri::command]
pub fn set_api_session(
Expand All @@ -20,20 +20,61 @@ pub fn set_api_org(state: State<'_, ApiSessionStore>, org_id: String) -> Result<
state.set_current_org(org_id)
}

async fn checked_token(
app: &AppHandle,
state: &ApiSessionStore,
site: &str,
validate_identity: bool,
) -> Result<String, String> {
let result = if validate_identity {
client_session::check_session(site).await
} else {
client_session::checked_token(site).await
};
match result {
Ok(token) => Ok(token),
Err(error) => {
if client_session::is_expired(&error) {
state.clear_site(site);
let _ = app.emit(
"client-session-expired",
serde_json::json!({ "site": site }),
);
}
Err(error.to_string())
}
}
}

#[tauri::command]
pub async fn check_client_session(
app: AppHandle,
state: State<'_, ApiSessionStore>,
site: String,
) -> Result<Option<client_session::ClientSessionInfo>, String> {
checked_token(&app, &state, &site, true).await?;
client_session::session_info(&site)
.await
.map_err(|error| error.to_string())
}

pub(crate) fn session_error_status(error: &str) -> u16 {
if error == client_session::EXPIRED {
401
} else {
0
}
}

pub(crate) async fn fresh_api_context(
_app: &AppHandle,
app: &AppHandle,
state: &ApiSessionStore,
) -> Result<ApiSessionContext, String> {
let mut context = state
.current_context()
.ok_or_else(|| "missing current api session".to_string())?;

let bearer = ensure_fresh_token(&context.origin, Some(&context.bearer_token))
.await
.map_err(|error| error.to_string())?;

state.update_current_bearer_token(bearer.clone())?;
let bearer = checked_token(app, state, &context.origin, false).await?;
state.update_bearer_token(&context.origin, bearer.clone())?;
context.bearer_token = bearer;

Ok(context)
}
6 changes: 3 additions & 3 deletions src-tauri/src/commands/asset_actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use crate::api::{
request::ApiRequestClient,
session::{ApiSessionContext, ApiSessionStore},
};
use crate::commands::api_session::fresh_api_context;
use crate::commands::api_session::{fresh_api_context, session_error_status};
use crate::service::asset::{AssetQuery, AssetService};
use log::{error, info};
use serde_json::{json, Value};
Expand Down Expand Up @@ -49,7 +49,7 @@ pub async fn get_assets(
error!("load asset service failed: {}", error);
let _ = app.emit(
"get-asset-failure",
json!({ "status": 401, "request_id": request_id }),
json!({ "status": session_error_status(&error), "request_id": request_id }),
);
return Ok(());
}
Expand Down Expand Up @@ -125,7 +125,7 @@ pub async fn get_asset_detail(
Err(error) => {
let _ = app.emit(
"get-asset-detail-failure",
json!({ "status": 401, "error": error }),
json!({ "status": session_error_status(&error), "error": error }),
);
return Ok(());
}
Expand Down
23 changes: 18 additions & 5 deletions src-tauri/src/commands/auth_flow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ pub async fn auth_login(
}
};

let policy_supported = oauth_config.client_login_policy_version.is_some()
|| oauth_config.client_login_policy.is_some();
let client_id = oauth_config.client_id;

let fut = async {
Expand Down Expand Up @@ -63,19 +65,29 @@ pub async fn auth_login(
let tokens = exchange_authorization_code(&client, &http_client, callback).await?;

// 保存 OAuth token,供后续请求自动刷新使用。
if let Err(e) = tokens.persist(&site, &client_id).await {
log::warn!("persist tokens failed: {}", e);
}
tokens
.persist(
&site,
&client_id,
oauth_config.client_login_policy,
policy_supported,
)
.await?;
let access_token = crate::service::client_session::checked_token(&site).await?;

// 发起请求
let user_service = UserService::new(site.clone(), tokens.access_token.clone())?;
let user_service = UserService::new(site.clone(), access_token.clone())?;
let (profile, permission_orgs, current_org, xpack_message) = tokio::join!(
user_service.get_user_profile(),
user_service.get_permission_orgs(),
user_service.get_current_org(),
user_service.get_xpack_message(),
);

if profile.status != 200 || !profile.success {
anyhow::bail!("Unable to validate the user profile");
}

let license_valid = if xpack_message.status == 200 && xpack_message.success {
serde_json::from_str::<Value>(&xpack_message.data)
.ok()
Expand All @@ -93,12 +105,13 @@ pub async fn auth_login(
"login-success-detected",
serde_json::json!({
"status": "success",
"bearer": tokens.access_token,
"bearer": access_token,
"profile": profile,
"resolved_site": site,
"current_org": current_org,
"xpack_license_valid": license_valid,
"permission_orgs": permission_orgs,
"client_login_session": crate::service::client_session::session_info(&site).await?,
}),
);

Expand Down
11 changes: 9 additions & 2 deletions src-tauri/src/commands/auth_logout.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
use crate::api::session::ApiSessionStore;
use crate::service::oauth::revoke_and_clear_tokens;
use log::warn;
use tauri::AppHandle;
use tauri::{AppHandle, State};

#[tauri::command]
pub async fn logout(_app: AppHandle, _name: String, site: String) -> Result<(), String> {
pub async fn logout(
_app: AppHandle,
state: State<'_, ApiSessionStore>,
_name: String,
site: String,
) -> Result<(), String> {
state.clear_site(&site);
if let Err(e) = revoke_and_clear_tokens(&site).await {
warn!("revoke token failed: {}", e);
}
Expand Down
4 changes: 2 additions & 2 deletions src-tauri/src/commands/connect_token.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use std::collections::HashMap;
use tauri::{AppHandle, Emitter, State};

use crate::api::{request::ApiRequestClient, session::ApiSessionStore};
use crate::commands::api_session::fresh_api_context;
use crate::commands::api_session::{fresh_api_context, session_error_status};
use crate::commands::client_launcher::pull_up;
use crate::service::connect::{ConnectService, TokenRequestBody};

Expand All @@ -20,7 +20,7 @@ pub async fn get_connect_token(
Err(e) => {
let _ = app.emit(
"get-token-failure",
json!({ "status": 401, "data": e.to_string() }),
json!({ "status": session_error_status(&e), "data": e.to_string() }),
);
return Ok(());
}
Expand Down
4 changes: 2 additions & 2 deletions src-tauri/src/commands/get_setting.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::api::{request::ApiRequestClient, session::ApiSessionStore};
use crate::commands::api_session::fresh_api_context;
use crate::commands::api_session::{fresh_api_context, session_error_status};
use crate::service::setting::SettingService;
use log::{error, info};
use serde_json::json;
Expand All @@ -15,7 +15,7 @@ pub async fn get_setting(
Err(e) => {
let _ = app.emit(
"get-setting-failure",
json!({ "status": 401, "error": e.to_string() }),
json!({ "status": session_error_status(&e), "error": e.to_string() }),
);
return Ok(());
}
Expand Down
25 changes: 22 additions & 3 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use crate::setup::menu::{build_menu, handle_menu_event};
use crate::setup::setup_tray;

use crate::api::session::ApiSessionStore;
use crate::commands::api_session::{set_api_org, set_api_session};
use crate::commands::api_session::{check_client_session, set_api_org, set_api_session};
use crate::commands::asset_actions::{
get_asset_detail, get_assets, rename, set_favorite, unfavorite,
};
Expand Down Expand Up @@ -209,13 +209,32 @@ pub fn run() {
update_config_selection,
init_http_callback_server,
get_connect_methods,
check_client_session,
set_api_session,
set_api_org,
write_video_player_gzip_file,
read_video_player_text_stream,
delete_video_player_file,
transcode_replays,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
.build(tauri::generate_context!())
.expect("error while building tauri application")
.run(|app, event| {
if let tauri::RunEvent::ExitRequested { api, code, .. } = event {
static EXITING: std::sync::atomic::AtomicBool =
std::sync::atomic::AtomicBool::new(false);
if !EXITING.swap(true, std::sync::atomic::Ordering::SeqCst) {
api.prevent_exit();
let app = app.clone();
tauri::async_runtime::spawn(async move {
let _ = tokio::time::timeout(
std::time::Duration::from_secs(3),
service::client_session::revoke_on_exit(),
)
.await;
app.exit(code.unwrap_or(0));
});
}
}
});
}
Loading
Loading