2
src-tauri/gen/schemas/acl-manifests.json
vendored
2
src-tauri/gen/schemas/acl-manifests.json
vendored
File diff suppressed because one or more lines are too long
@@ -1,4 +1,5 @@
|
||||
use crate::AppData;
|
||||
use crate::command::token_helper::{capture_token_snapshot_arc, persist_token_if_refreshed_arc};
|
||||
use crate::error::CommonError;
|
||||
use crate::im_request_client::{ImRequestClient, ImUrl};
|
||||
use crate::repository::im_contact_repository::{
|
||||
@@ -71,6 +72,8 @@ async fn fetch_and_update_contacts(
|
||||
request_client: Arc<Mutex<ImRequestClient>>,
|
||||
login_uid: String,
|
||||
) -> Result<Vec<im_contact::Model>, CommonError> {
|
||||
let old_tokens = capture_token_snapshot_arc(&request_client).await;
|
||||
|
||||
let resp: Option<Vec<im_contact::Model>> = request_client
|
||||
.lock()
|
||||
.await
|
||||
@@ -81,6 +84,8 @@ async fn fetch_and_update_contacts(
|
||||
)
|
||||
.await?;
|
||||
|
||||
persist_token_if_refreshed_arc(&old_tokens, &request_client, &db_conn, &login_uid).await;
|
||||
|
||||
if let Some(data) = resp {
|
||||
// 保存到本地数据库
|
||||
save_contact_batch(&*db_conn.read().await, data.clone(), &login_uid)
|
||||
@@ -122,6 +127,8 @@ pub async fn hide_contact_command(
|
||||
user_info.uid.clone()
|
||||
};
|
||||
|
||||
let old_tokens = capture_token_snapshot_arc(&state.rc).await;
|
||||
|
||||
let resp: Option<bool> = state
|
||||
.rc
|
||||
.lock()
|
||||
@@ -133,6 +140,8 @@ pub async fn hide_contact_command(
|
||||
)
|
||||
.await?;
|
||||
|
||||
persist_token_if_refreshed_arc(&old_tokens, &state.rc, &state.db_conn, &login_uid).await;
|
||||
|
||||
if let Some(_) = resp {
|
||||
// 更新本地数据库
|
||||
update_contact_hide(
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
use crate::AppData;
|
||||
use crate::command::token_helper::{
|
||||
capture_token_snapshot_arc, capture_token_snapshot_direct, persist_token_if_refreshed_arc,
|
||||
persist_token_if_refreshed_direct,
|
||||
};
|
||||
use crate::error::CommonError;
|
||||
use crate::im_request_client::{ImRequestClient, ImUrl};
|
||||
use crate::pojo::common::{CursorPageParam, CursorPageResp};
|
||||
@@ -382,10 +386,14 @@ pub async fn fetch_all_messages(
|
||||
false => None,
|
||||
};
|
||||
|
||||
let old_tokens = capture_token_snapshot_direct(client);
|
||||
|
||||
let messages: Option<Vec<MessageResp>> = client
|
||||
.im_request(ImUrl::GetMsgList, body, None::<serde_json::Value>)
|
||||
.await?;
|
||||
|
||||
persist_token_if_refreshed_direct(&old_tokens, client, db_conn, uid).await;
|
||||
|
||||
if let Some(mut messages) = messages {
|
||||
// 排序消息(按发送时间)
|
||||
messages.sort_by(|a, b| {
|
||||
@@ -656,8 +664,11 @@ pub async fn send_msg(
|
||||
let db_conn = state.db_conn.clone();
|
||||
let request_client = state.rc.clone();
|
||||
let mut record_for_send = message_record.clone();
|
||||
let uid_for_token = login_uid.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let old_tokens = capture_token_snapshot_arc(&request_client).await;
|
||||
|
||||
// 发送到后端接口
|
||||
let result: Result<Option<MessageResp>, anyhow::Error> = {
|
||||
let mut client = request_client.lock().await;
|
||||
@@ -666,6 +677,9 @@ pub async fn send_msg(
|
||||
.await
|
||||
};
|
||||
|
||||
persist_token_if_refreshed_arc(&old_tokens, &request_client, &db_conn, &uid_for_token)
|
||||
.await;
|
||||
|
||||
let mut id = None;
|
||||
|
||||
// 根据发送结果更新消息状态
|
||||
|
||||
@@ -15,6 +15,7 @@ pub mod oauth_command;
|
||||
pub mod request_command;
|
||||
pub mod room_member_command;
|
||||
pub mod setting_command;
|
||||
pub mod token_helper;
|
||||
pub mod upload_command;
|
||||
pub mod user_command;
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
use migration::{Migrator, MigratorTrait};
|
||||
use tauri::{AppHandle, Emitter, State};
|
||||
use tracing::{error, info};
|
||||
use tauri::{AppHandle, Emitter, Manager, State};
|
||||
|
||||
use crate::{
|
||||
AppData,
|
||||
command::message_command::check_user_init_and_fetch_messages,
|
||||
command::token_helper::capture_token_snapshot_direct,
|
||||
configuration::get_configuration,
|
||||
im_request_client::{ImRequest, ImUrl},
|
||||
repository::{im_message_repository::reset_table_initialization_flags, im_user_repository},
|
||||
vo::vo::{LoginReq, LoginResp, RefreshTokenReq},
|
||||
vo::vo::{LoginReq, LoginResp},
|
||||
};
|
||||
|
||||
#[tauri::command]
|
||||
@@ -20,72 +20,88 @@ pub async fn login_command(
|
||||
if data.is_auto_login {
|
||||
// 自动登录逻辑
|
||||
if let Some(uid) = &data.uid {
|
||||
info!("Attempting auto login, user ID: {}", uid);
|
||||
|
||||
// 先切换到用户专属数据库
|
||||
switch_to_user_database(&state, &app_handle, uid).await?;
|
||||
|
||||
// 从数据库获取用户的 refresh_token
|
||||
match im_user_repository::get_user_tokens(&*state.db_conn.read().await, uid).await {
|
||||
Ok(Some((_, refresh_token))) => {
|
||||
info!(
|
||||
"Found refresh_token for user {}, attempting to refresh login",
|
||||
uid
|
||||
);
|
||||
let db_result =
|
||||
im_user_repository::get_user_tokens(&*state.db_conn.read().await, uid).await;
|
||||
match db_result {
|
||||
Ok(Some((token, refresh_token))) => {
|
||||
if refresh_token.is_empty() {
|
||||
let check_result: Result<Option<serde_json::Value>, anyhow::Error> = {
|
||||
let mut rc = state.rc.lock().await;
|
||||
rc.token = Some(token.clone());
|
||||
rc.im_request(
|
||||
ImUrl::CheckToken,
|
||||
None::<serde_json::Value>,
|
||||
None::<serde_json::Value>,
|
||||
)
|
||||
.await
|
||||
};
|
||||
|
||||
// 使用 refresh_token 刷新登录
|
||||
let refresh_req = RefreshTokenReq {
|
||||
refresh_token: refresh_token.clone(),
|
||||
};
|
||||
if check_result.is_ok() {
|
||||
let login_resp = LoginResp {
|
||||
token,
|
||||
client: "".to_string(),
|
||||
refresh_token: refresh_token.clone(),
|
||||
expire: "".to_string(),
|
||||
uid: uid.clone(),
|
||||
};
|
||||
handle_login_success(&login_resp, &state, data.async_data).await?;
|
||||
return Ok(Some(login_resp));
|
||||
}
|
||||
}
|
||||
|
||||
// 使用 start_refresh_token 刷新登录(不会添加 token 头,适合自动登录场景)
|
||||
let refresh_result = {
|
||||
let mut rc = state.rc.lock().await;
|
||||
rc.refresh_token(refresh_req).await
|
||||
// 设置 ImRequestClient 内部的 refresh_token
|
||||
rc.refresh_token = Some(refresh_token.clone());
|
||||
rc.start_refresh_token().await
|
||||
};
|
||||
|
||||
match refresh_result {
|
||||
Ok(Some(refresh_resp)) => {
|
||||
info!("Auto login successful, user ID: {}", uid);
|
||||
Ok(()) => {
|
||||
// 从 ImRequestClient 中获取刷新后的 token
|
||||
let rc = state.rc.lock().await;
|
||||
let new_token = rc.token.clone().unwrap_or_default();
|
||||
let new_refresh_token = rc.refresh_token.clone().unwrap_or_default();
|
||||
drop(rc);
|
||||
|
||||
// 保存新的 token 信息到数据库
|
||||
if let Err(e) = im_user_repository::save_user_tokens(
|
||||
im_user_repository::save_user_tokens(
|
||||
&*state.db_conn.read().await,
|
||||
uid,
|
||||
&refresh_resp.token,
|
||||
&refresh_resp.refresh_token,
|
||||
&new_token,
|
||||
&new_refresh_token,
|
||||
)
|
||||
.await
|
||||
{
|
||||
error!("Failed to save new token info: {}", e);
|
||||
}
|
||||
.ok();
|
||||
|
||||
// 转换为 LoginResp 格式返回
|
||||
let login_resp = LoginResp {
|
||||
token: refresh_resp.token,
|
||||
client: "".to_string(), // refresh_token 响应通常不包含 client
|
||||
refresh_token: refresh_resp.refresh_token,
|
||||
expire: refresh_resp.expire,
|
||||
uid: refresh_resp.uid,
|
||||
token: new_token,
|
||||
client: "".to_string(),
|
||||
refresh_token: new_refresh_token,
|
||||
expire: "".to_string(),
|
||||
uid: uid.clone(),
|
||||
};
|
||||
|
||||
handle_login_success(&login_resp, &state, data.async_data).await?;
|
||||
|
||||
return Ok(Some(login_resp));
|
||||
}
|
||||
Ok(None) => {
|
||||
error!("Auto login failed: refresh token returned empty result");
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Auto login failed: refresh token request failed: {}", e);
|
||||
let err_str = e.to_string();
|
||||
if err_str.contains("network_error") {
|
||||
return Err("网络连接失败,请检查网络后重试".to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(None) => {
|
||||
info!("User {} has no saved token info, cannot auto login", uid);
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to get token info for user {}: {}", uid, e);
|
||||
}
|
||||
Ok(None) => {}
|
||||
Err(_) => {}
|
||||
};
|
||||
// 自动登录失败,返回错误让前端切换到手动登录
|
||||
return Err("自动登录失败,请手动登录".to_string());
|
||||
@@ -94,8 +110,6 @@ pub async fn login_command(
|
||||
}
|
||||
} else {
|
||||
// 手动登录逻辑
|
||||
info!("Performing manual login");
|
||||
|
||||
let async_data = data.async_data;
|
||||
let res = {
|
||||
let mut rc = state.rc.lock().await;
|
||||
@@ -109,7 +123,6 @@ pub async fn login_command(
|
||||
handle_login_success(login_resp, &state, async_data).await?;
|
||||
}
|
||||
|
||||
info!("Manual login successful");
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
@@ -120,8 +133,6 @@ async fn switch_to_user_database(
|
||||
app_handle: &AppHandle,
|
||||
uid: &str,
|
||||
) -> Result<(), String> {
|
||||
info!("Switching to user database for uid: {}", uid);
|
||||
|
||||
// 获取配置
|
||||
let configuration = get_configuration(app_handle)
|
||||
.map_err(|e| format!("Failed to load configuration: {}", e))?;
|
||||
@@ -134,13 +145,8 @@ async fn switch_to_user_database(
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
// 执行数据库迁移
|
||||
match Migrator::up(&new_db, None).await {
|
||||
Ok(_) => {
|
||||
info!("Database migration completed for user: {}", uid);
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!("Database migration warning for user {}: {}", uid, e);
|
||||
}
|
||||
if let Err(e) = Migrator::up(&new_db, None).await {
|
||||
tracing::warn!("Database migration warning for user {}: {}", uid, e);
|
||||
}
|
||||
|
||||
// 重置表初始化标志,确保新数据库会创建必要的表
|
||||
@@ -152,7 +158,6 @@ async fn switch_to_user_database(
|
||||
*db_guard = new_db;
|
||||
}
|
||||
|
||||
info!("Successfully switched to database for user: {}", uid);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -161,7 +166,6 @@ async fn handle_login_success(
|
||||
state: &State<'_, AppData>,
|
||||
async_data: bool,
|
||||
) -> Result<(), String> {
|
||||
info!("handle_login_success, login_resp: {:?}", login_resp);
|
||||
// 从登录响应中获取用户标识,这里使用 uid 作为 uid
|
||||
let uid = &login_resp.uid;
|
||||
|
||||
@@ -170,7 +174,6 @@ async fn handle_login_success(
|
||||
user_info.uid = login_resp.uid.clone();
|
||||
user_info.token = login_resp.token.clone();
|
||||
user_info.refresh_token = login_resp.refresh_token.clone();
|
||||
info!("handle_login_success, user_info: {:?}", user_info);
|
||||
// 保存 token 信息到数据库
|
||||
im_user_repository::save_user_tokens(
|
||||
&*state.db_conn.read().await,
|
||||
@@ -204,26 +207,62 @@ pub async fn im_request_command(
|
||||
params: Option<serde_json::Value>,
|
||||
app_handle: tauri::AppHandle,
|
||||
) -> Result<Option<serde_json::Value>, String> {
|
||||
let user_uid = state.user_info.lock().await.uid.clone();
|
||||
let mut rc = state.rc.lock().await;
|
||||
|
||||
// 记录请求前的 token,用于检测是否被刷新
|
||||
let old_tokens = capture_token_snapshot_direct(&rc);
|
||||
|
||||
if let Ok(url) = url.parse::<ImUrl>() {
|
||||
let result: Result<Option<serde_json::Value>, anyhow::Error> =
|
||||
rc.im_request(url, body, params).await;
|
||||
|
||||
// 无论请求成功还是失败,都检查 token 是否被刷新,如果是则保存到数据库
|
||||
// 这确保了即使请求重试后失败,刷新后的 token 也能被持久化
|
||||
if let (Some(new_token), Some(new_refresh_token)) =
|
||||
(rc.token.clone(), rc.refresh_token.clone())
|
||||
{
|
||||
if old_tokens.token != rc.token || old_tokens.refresh_token != rc.refresh_token {
|
||||
if !user_uid.is_empty() {
|
||||
im_user_repository::save_user_tokens(
|
||||
&*state.db_conn.read().await,
|
||||
&user_uid,
|
||||
&new_token,
|
||||
&new_refresh_token,
|
||||
)
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match result {
|
||||
Ok(data) => {
|
||||
return Ok(data);
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Request error: {}", e);
|
||||
if e.to_string().contains("请重新登录") {
|
||||
app_handle.emit_to("home", "relogin", ()).unwrap();
|
||||
if user_uid.is_empty() {
|
||||
} else {
|
||||
if app_handle.get_webview_window("home").is_some() {
|
||||
if let Err(err) = app_handle.emit_to("home", "relogin", ()) {
|
||||
let _ = err;
|
||||
}
|
||||
} else if app_handle.get_webview_window("mobile-home").is_some() {
|
||||
if let Err(err) = app_handle.emit_to("mobile-home", "relogin", ()) {
|
||||
let _ = err;
|
||||
}
|
||||
} else {
|
||||
if let Err(err) = app_handle.emit("relogin", ()) {
|
||||
let _ = err;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return Err(e.to_string());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
tracing::error!("Invalid URL: {}", url);
|
||||
return Err(format!("Invalid URL: {}", url));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::AppData;
|
||||
use crate::command::token_helper::{capture_token_snapshot_arc, persist_token_if_refreshed_arc};
|
||||
use crate::error::CommonError;
|
||||
use crate::pojo::common::{CursorPageParam, CursorPageResp, Page, PageParam};
|
||||
use crate::repository::im_room_member_repository::update_my_room_info as update_my_room_info_db;
|
||||
@@ -51,6 +52,8 @@ pub async fn update_my_room_info(
|
||||
let uid = user_info.uid.clone();
|
||||
drop(user_info);
|
||||
|
||||
let old_tokens = capture_token_snapshot_arc(&state.rc).await;
|
||||
|
||||
// 调用后端接口更新房间信息
|
||||
let _resp: Option<bool> = state
|
||||
.rc
|
||||
@@ -63,6 +66,8 @@ pub async fn update_my_room_info(
|
||||
)
|
||||
.await?;
|
||||
|
||||
persist_token_if_refreshed_arc(&old_tokens, &state.rc, &state.db_conn, &uid).await;
|
||||
|
||||
// 更新本地数据库
|
||||
update_my_room_info_db(
|
||||
&*state.db_conn.read().await,
|
||||
@@ -100,8 +105,15 @@ pub async fn get_room_members(
|
||||
state: State<'_, AppData>,
|
||||
) -> Result<Vec<RoomMemberResponse>, String> {
|
||||
info!("Calling to get all member list of room with room_id");
|
||||
let uid = state.user_info.lock().await.uid.clone();
|
||||
let result: Result<Vec<RoomMemberResponse>, CommonError> = async {
|
||||
let mut members = fetch_and_update_room_members(room_id.clone(), state.rc.clone()).await?;
|
||||
let mut members = fetch_and_update_room_members(
|
||||
room_id.clone(),
|
||||
state.rc.clone(),
|
||||
state.db_conn.clone(),
|
||||
&uid,
|
||||
)
|
||||
.await?;
|
||||
|
||||
sort_room_members(&mut members);
|
||||
|
||||
@@ -155,9 +167,12 @@ pub async fn page_room(
|
||||
page_param: PageParam,
|
||||
state: State<'_, AppData>,
|
||||
) -> Result<Page<im_room::Model>, String> {
|
||||
let uid = state.user_info.lock().await.uid.clone();
|
||||
let result: Result<Page<im_room::Model>, CommonError> = async {
|
||||
// 直接调用后端接口获取数据,不保存到数据库
|
||||
let data = fetch_rooms_from_backend(page_param, state.rc.clone()).await?;
|
||||
let data =
|
||||
fetch_rooms_from_backend(page_param, state.rc.clone(), state.db_conn.clone(), &uid)
|
||||
.await?;
|
||||
|
||||
Ok(data)
|
||||
}
|
||||
@@ -176,16 +191,23 @@ pub async fn page_room(
|
||||
async fn fetch_rooms_from_backend(
|
||||
page_param: PageParam,
|
||||
request_client: Arc<Mutex<ImRequestClient>>,
|
||||
db_conn: Arc<tokio::sync::RwLock<sea_orm::DatabaseConnection>>,
|
||||
uid: &str,
|
||||
) -> Result<Page<im_room::Model>, CommonError> {
|
||||
let mut client = request_client.lock().await;
|
||||
let old_tokens = capture_token_snapshot_arc(&request_client).await;
|
||||
|
||||
let resp: Option<Page<im_room::Model>> = client
|
||||
.im_request(
|
||||
ImUrl::GroupList,
|
||||
None::<serde_json::Value>,
|
||||
Some(page_param),
|
||||
)
|
||||
.await?;
|
||||
let resp: Option<Page<im_room::Model>> = {
|
||||
let mut client = request_client.lock().await;
|
||||
client
|
||||
.im_request(
|
||||
ImUrl::GroupList,
|
||||
None::<serde_json::Value>,
|
||||
Some(page_param),
|
||||
)
|
||||
.await?
|
||||
};
|
||||
|
||||
persist_token_if_refreshed_arc(&old_tokens, &request_client, &db_conn, uid).await;
|
||||
|
||||
if let Some(data) = resp {
|
||||
Ok(data)
|
||||
@@ -223,7 +245,11 @@ fn sort_room_members(members: &mut Vec<RoomMemberResponse>) {
|
||||
async fn fetch_and_update_room_members(
|
||||
room_id: String,
|
||||
request_client: Arc<Mutex<ImRequestClient>>,
|
||||
db_conn: Arc<tokio::sync::RwLock<sea_orm::DatabaseConnection>>,
|
||||
uid: &str,
|
||||
) -> Result<Vec<RoomMemberResponse>, CommonError> {
|
||||
let old_tokens = capture_token_snapshot_arc(&request_client).await;
|
||||
|
||||
let resp: Option<Vec<RoomMemberResponse>> = request_client
|
||||
.lock()
|
||||
.await
|
||||
@@ -236,6 +262,8 @@ async fn fetch_and_update_room_members(
|
||||
)
|
||||
.await?;
|
||||
|
||||
persist_token_if_refreshed_arc(&old_tokens, &request_client, &db_conn, uid).await;
|
||||
|
||||
if let Some(data) = resp {
|
||||
return Ok(data);
|
||||
}
|
||||
|
||||
202
src-tauri/src/command/token_helper.rs
Normal file
202
src-tauri/src/command/token_helper.rs
Normal file
@@ -0,0 +1,202 @@
|
||||
use crate::im_request_client::ImRequestClient;
|
||||
use crate::repository::im_user_repository;
|
||||
use sea_orm::DatabaseConnection;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::{Mutex, RwLock};
|
||||
use tracing::{info, warn};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct TokenSnapshot {
|
||||
pub token: Option<String>,
|
||||
pub refresh_token: Option<String>,
|
||||
}
|
||||
|
||||
/// Token 刷新检测器
|
||||
/// 用于在 im_request 调用前后检测 token 是否被刷新,并自动持久化到数据库
|
||||
pub struct TokenRefreshGuard {
|
||||
old_tokens: TokenSnapshot,
|
||||
}
|
||||
|
||||
impl TokenRefreshGuard {
|
||||
/// 在 im_request 调用前创建,记录当前的 token 和 refresh_token
|
||||
pub async fn before_request(client: &Mutex<ImRequestClient>) -> Self {
|
||||
let old_tokens = capture_token_snapshot(client).await;
|
||||
Self { old_tokens }
|
||||
}
|
||||
|
||||
/// 在 im_request 调用后检查 token 是否被刷新,如果是则持久化到数据库
|
||||
pub async fn persist_if_refreshed(
|
||||
&self,
|
||||
client: &Mutex<ImRequestClient>,
|
||||
db_conn: &RwLock<DatabaseConnection>,
|
||||
uid: &str,
|
||||
) {
|
||||
if uid.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let (new_token, new_refresh_token, token_changed) = {
|
||||
let c = client.lock().await;
|
||||
let changed = self.old_tokens.token != c.token
|
||||
|| self.old_tokens.refresh_token != c.refresh_token;
|
||||
(c.token.clone(), c.refresh_token.clone(), changed)
|
||||
};
|
||||
|
||||
if token_changed {
|
||||
if let (Some(token), Some(refresh_token)) = (new_token, new_refresh_token) {
|
||||
match im_user_repository::save_user_tokens(
|
||||
&*db_conn.read().await,
|
||||
uid,
|
||||
&token,
|
||||
&refresh_token,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
info!(
|
||||
"[TOKEN_PERSIST] SUCCESS: tokens saved for uid: {}, token_len: {}, refresh_len: {}",
|
||||
uid,
|
||||
token.len(),
|
||||
refresh_token.len()
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(
|
||||
"[TOKEN_PERSIST] FAILED: error saving tokens for uid {}: {}",
|
||||
uid, e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 便捷函数:在 im_request 后检查并持久化刷新的 token
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `old_tokens` - 请求前的 token 快照
|
||||
/// * `client` - ImRequestClient 的锁
|
||||
/// * `db_conn` - 数据库连接的锁
|
||||
/// * `uid` - 用户 ID
|
||||
pub async fn persist_token_if_refreshed(
|
||||
old_tokens: &TokenSnapshot,
|
||||
client: &Mutex<ImRequestClient>,
|
||||
db_conn: &RwLock<DatabaseConnection>,
|
||||
uid: &str,
|
||||
) {
|
||||
if uid.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let (new_token, new_refresh_token, token_changed) = {
|
||||
let c = client.lock().await;
|
||||
let changed = old_tokens.token != c.token || old_tokens.refresh_token != c.refresh_token;
|
||||
(c.token.clone(), c.refresh_token.clone(), changed)
|
||||
};
|
||||
|
||||
if token_changed {
|
||||
if let (Some(token), Some(refresh_token)) = (new_token, new_refresh_token) {
|
||||
match im_user_repository::save_user_tokens(
|
||||
&*db_conn.read().await,
|
||||
uid,
|
||||
&token,
|
||||
&refresh_token,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
info!(
|
||||
"[TOKEN_PERSIST] SUCCESS: tokens saved for uid: {}, token_len: {}, refresh_len: {}",
|
||||
uid,
|
||||
token.len(),
|
||||
refresh_token.len()
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(
|
||||
"[TOKEN_PERSIST] FAILED: error saving tokens for uid {}: {}",
|
||||
uid, e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 便捷函数:获取当前的 token 快照用于后续比较
|
||||
pub async fn capture_token_snapshot(client: &Mutex<ImRequestClient>) -> TokenSnapshot {
|
||||
let c = client.lock().await;
|
||||
TokenSnapshot {
|
||||
token: c.token.clone(),
|
||||
refresh_token: c.refresh_token.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn capture_token_snapshot_arc(client: &Arc<Mutex<ImRequestClient>>) -> TokenSnapshot {
|
||||
capture_token_snapshot(client.as_ref()).await
|
||||
}
|
||||
|
||||
pub fn capture_token_snapshot_direct(client: &ImRequestClient) -> TokenSnapshot {
|
||||
TokenSnapshot {
|
||||
token: client.token.clone(),
|
||||
refresh_token: client.refresh_token.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 便捷函数:获取当前的 refresh_token 用于后续比较
|
||||
pub async fn capture_refresh_token(client: &Mutex<ImRequestClient>) -> Option<String> {
|
||||
client.lock().await.refresh_token.clone()
|
||||
}
|
||||
|
||||
/// 便捷函数:使用 Arc 包装的类型
|
||||
pub async fn persist_token_if_refreshed_arc(
|
||||
old_tokens: &TokenSnapshot,
|
||||
client: &Arc<Mutex<ImRequestClient>>,
|
||||
db_conn: &Arc<RwLock<DatabaseConnection>>,
|
||||
uid: &str,
|
||||
) {
|
||||
persist_token_if_refreshed(old_tokens, client.as_ref(), db_conn.as_ref(), uid).await
|
||||
}
|
||||
|
||||
pub async fn capture_refresh_token_arc(client: &Arc<Mutex<ImRequestClient>>) -> Option<String> {
|
||||
capture_refresh_token(client.as_ref()).await
|
||||
}
|
||||
|
||||
/// 便捷函数:使用直接引用的 ImRequestClient(不需要 Mutex)
|
||||
pub async fn persist_token_if_refreshed_direct(
|
||||
old_tokens: &TokenSnapshot,
|
||||
client: &ImRequestClient,
|
||||
db_conn: &DatabaseConnection,
|
||||
uid: &str,
|
||||
) {
|
||||
if uid.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let token_changed =
|
||||
old_tokens.token != client.token || old_tokens.refresh_token != client.refresh_token;
|
||||
|
||||
if token_changed {
|
||||
if let (Some(token), Some(refresh_token)) =
|
||||
(client.token.clone(), client.refresh_token.clone())
|
||||
{
|
||||
match im_user_repository::save_user_tokens(db_conn, uid, &token, &refresh_token).await {
|
||||
Ok(_) => {
|
||||
info!(
|
||||
"[TOKEN_PERSIST] SUCCESS: tokens saved for uid: {}, token_len: {}, refresh_len: {}",
|
||||
uid,
|
||||
token.len(),
|
||||
refresh_token.len()
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(
|
||||
"[TOKEN_PERSIST] FAILED: error saving tokens for uid {}: {}",
|
||||
uid, e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -136,22 +136,34 @@ pub async fn update_token(
|
||||
state: State<'_, AppData>,
|
||||
) -> Result<(), String> {
|
||||
info!("Updating user token");
|
||||
let refresh_token = if req.refresh_token.is_empty() {
|
||||
let current_refresh = state.user_info.lock().await.refresh_token.clone();
|
||||
if current_refresh.is_empty() {
|
||||
"".to_string()
|
||||
} else {
|
||||
current_refresh
|
||||
}
|
||||
} else {
|
||||
req.refresh_token.clone()
|
||||
};
|
||||
{
|
||||
let mut user_info = state.user_info.lock().await;
|
||||
user_info.uid = req.uid.clone();
|
||||
user_info.token = req.token.clone();
|
||||
user_info.refresh_token = req.refresh_token.clone();
|
||||
user_info.refresh_token = refresh_token.clone();
|
||||
}
|
||||
{
|
||||
let mut rc = state.rc.lock().await;
|
||||
rc.token = Some(req.token.clone());
|
||||
rc.refresh_token = Some(req.refresh_token.clone());
|
||||
if !refresh_token.is_empty() {
|
||||
rc.refresh_token = Some(refresh_token.clone());
|
||||
}
|
||||
}
|
||||
im_user_repository::save_user_tokens(
|
||||
&*state.db_conn.read().await,
|
||||
&req.uid,
|
||||
&req.token,
|
||||
&req.refresh_token,
|
||||
&refresh_token,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::Ok;
|
||||
use base64::{Engine, prelude::BASE64_STANDARD};
|
||||
use reqwest::header;
|
||||
use serde_json::json;
|
||||
use tracing::{error, info};
|
||||
use tracing::error;
|
||||
|
||||
use crate::{
|
||||
pojo::common::ApiResult,
|
||||
vo::vo::{LoginReq, LoginResp, RefreshTokenReq, RefreshTokenResp},
|
||||
vo::vo::{LoginReq, LoginResp},
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -67,8 +66,6 @@ impl ImRequestClient {
|
||||
extra_headers: Option<Vec<(&str, &str)>>,
|
||||
) -> reqwest::RequestBuilder {
|
||||
let url = format!("{}/{}", self.base_url, path);
|
||||
info!("Request URL: {}, Method: {}", &url, method);
|
||||
|
||||
let mut request_builder = self.client.request(method, &url);
|
||||
|
||||
// 设置 token 请求头
|
||||
@@ -116,9 +113,15 @@ impl ImRequestClient {
|
||||
// 使用 build_request 构建请求
|
||||
let request_builder = self.build_request(method.clone(), path, &body, ¶ms, None);
|
||||
|
||||
// 发送请求
|
||||
let response = request_builder.send().await?;
|
||||
let result: ApiResult<T> = response.json().await?;
|
||||
// 发送请求,区分网络错误和业务错误
|
||||
let response = request_builder
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("network_error: {}", e))?;
|
||||
let result: ApiResult<T> = response
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("network_error: {}", e))?;
|
||||
|
||||
let url = format!("{}/{}", self.base_url, path);
|
||||
|
||||
@@ -127,8 +130,6 @@ impl ImRequestClient {
|
||||
if retry_count >= MAX_RETRY_COUNT {
|
||||
return Err(anyhow::anyhow!("token过期,刷新token失败"));
|
||||
}
|
||||
|
||||
error!("Token expired, starting token refresh");
|
||||
self.start_refresh_token().await?;
|
||||
retry_count += 1;
|
||||
continue;
|
||||
@@ -143,7 +144,6 @@ impl ImRequestClient {
|
||||
return Err(anyhow::anyhow!("请重新登录"));
|
||||
}
|
||||
Some(200) => {
|
||||
info!("Request successful: {}, Method: {}", &url, method.clone());
|
||||
return Ok(result);
|
||||
}
|
||||
_ => {
|
||||
@@ -217,44 +217,60 @@ impl ImRequestClient {
|
||||
}
|
||||
}
|
||||
|
||||
info!("流式请求成功,开始接收流式数据");
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
pub async fn start_refresh_token(&mut self) -> Result<(), anyhow::Error> {
|
||||
info!("Starting token refresh");
|
||||
let url = format!("{}/{}", self.base_url, ImUrl::RefreshToken.get_url().1);
|
||||
|
||||
let refresh_token = match self.refresh_token.clone() {
|
||||
Some(token) if !token.is_empty() => token,
|
||||
_ => return Err(anyhow::anyhow!("请重新登录")),
|
||||
};
|
||||
let old_refresh_token = refresh_token.clone();
|
||||
|
||||
let body = json!({
|
||||
"refreshToken": self.refresh_token.clone().unwrap()
|
||||
"refreshToken": refresh_token
|
||||
});
|
||||
|
||||
let request_builder = self.client.request(http::Method::POST, &url);
|
||||
let response = request_builder.json(&body).send().await?;
|
||||
let result: ApiResult<serde_json::Value> = response.json().await?;
|
||||
let response = request_builder
|
||||
.json(&body)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("network_error: {}", e))?;
|
||||
let result: ApiResult<serde_json::Value> = response
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("network_error: {}", e))?;
|
||||
|
||||
if !result.success {
|
||||
error!("刷新token失败: {}", result.msg.clone().unwrap_or_default());
|
||||
return Err(anyhow::anyhow!(
|
||||
"刷新token失败: {}",
|
||||
result.msg.clone().unwrap_or_default()
|
||||
));
|
||||
// 服务器明确拒绝 refresh token,需要重新登录
|
||||
return Err(anyhow::anyhow!("请重新登录"));
|
||||
}
|
||||
|
||||
if let Some(token) = result.data.clone().unwrap().get("token").unwrap().as_str() {
|
||||
self.token = Some(token.to_owned());
|
||||
}
|
||||
let data = match result.data {
|
||||
Some(data) => data,
|
||||
None => {
|
||||
error!("刷新token失败: 响应缺少 data 字段");
|
||||
return Err(anyhow::anyhow!("请重新登录"));
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(refresh_token) = result
|
||||
.data
|
||||
.clone()
|
||||
.unwrap()
|
||||
.get("refreshToken")
|
||||
.unwrap()
|
||||
.as_str()
|
||||
{
|
||||
self.refresh_token = Some(refresh_token.to_owned());
|
||||
}
|
||||
let token = match data.get("token").and_then(|value| value.as_str()) {
|
||||
Some(value) => value,
|
||||
None => {
|
||||
error!("刷新token失败: 响应缺少 token 字段");
|
||||
return Err(anyhow::anyhow!("请重新登录"));
|
||||
}
|
||||
};
|
||||
self.token = Some(token.to_owned());
|
||||
|
||||
let refresh_token = match data.get("refreshToken").and_then(|value| value.as_str()) {
|
||||
Some(value) if !value.is_empty() => value.to_owned(),
|
||||
_ => old_refresh_token,
|
||||
};
|
||||
self.refresh_token = Some(refresh_token);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -288,26 +304,6 @@ impl ImRequest for ImRequestClient {
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
async fn refresh_token(
|
||||
&mut self,
|
||||
refresh_token_req: RefreshTokenReq,
|
||||
) -> Result<Option<RefreshTokenResp>, anyhow::Error> {
|
||||
let result: Option<RefreshTokenResp> = self
|
||||
.im_request(
|
||||
ImUrl::RefreshToken,
|
||||
Some(refresh_token_req),
|
||||
None::<serde_json::Value>,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(data) = result.clone() {
|
||||
self.token = Some(data.token.clone());
|
||||
self.refresh_token = Some(data.refresh_token.clone());
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
pub enum ImUrl {
|
||||
@@ -1145,8 +1141,4 @@ impl FromStr for ImUrl {
|
||||
|
||||
pub trait ImRequest {
|
||||
async fn login(&mut self, login_req: LoginReq) -> Result<Option<LoginResp>, anyhow::Error>;
|
||||
async fn refresh_token(
|
||||
&mut self,
|
||||
refresh_token_req: RefreshTokenReq,
|
||||
) -> Result<Option<RefreshTokenResp>, anyhow::Error>;
|
||||
}
|
||||
|
||||
@@ -184,7 +184,7 @@ async fn initialize_app_data(
|
||||
let rc: im_request_client::ImRequestClient = im_request_client::ImRequestClient::new(
|
||||
configuration.lock().await.backend.base_url.clone(),
|
||||
)
|
||||
.unwrap();
|
||||
.map_err(|e| anyhow::anyhow!("Failed to create request client: {}", e))?;
|
||||
|
||||
// 创建用户信息
|
||||
let user_info = UserInfo {
|
||||
@@ -343,7 +343,9 @@ fn setup_mobile() {
|
||||
// 公共的 setup 函数
|
||||
fn common_setup(app_handle: AppHandle) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let scope = app_handle.fs_scope();
|
||||
scope.allow_directory("configuration", false).unwrap();
|
||||
if let Err(e) = scope.allow_directory("configuration", false) {
|
||||
tracing::warn!("Failed to allow configuration directory: {}", e);
|
||||
}
|
||||
|
||||
#[cfg(desktop)]
|
||||
setup_logout_listener(app_handle.clone());
|
||||
|
||||
@@ -51,12 +51,30 @@ where
|
||||
CommonError::DatabaseError(e)
|
||||
})?;
|
||||
|
||||
let refresh_token_to_save = if refresh_token.is_empty() {
|
||||
if let Some(user) = &existing_user {
|
||||
if let Some(saved_refresh) = user.refresh_token.clone() {
|
||||
if !saved_refresh.is_empty() {
|
||||
saved_refresh
|
||||
} else {
|
||||
"".to_string()
|
||||
}
|
||||
} else {
|
||||
"".to_string()
|
||||
}
|
||||
} else {
|
||||
"".to_string()
|
||||
}
|
||||
} else {
|
||||
refresh_token.to_string()
|
||||
};
|
||||
|
||||
let user_update = if existing_user.is_some() {
|
||||
// 用户存在,更新 token 信息
|
||||
im_user::ActiveModel {
|
||||
id: Set(login_uid.to_string()),
|
||||
token: Set(Some(token.to_string())),
|
||||
refresh_token: Set(Some(refresh_token.to_string())),
|
||||
refresh_token: Set(Some(refresh_token_to_save.clone())),
|
||||
..Default::default()
|
||||
}
|
||||
} else {
|
||||
@@ -64,7 +82,7 @@ where
|
||||
im_user::ActiveModel {
|
||||
id: Set(login_uid.to_string()),
|
||||
token: Set(Some(token.to_string())),
|
||||
refresh_token: Set(Some(refresh_token.to_string())),
|
||||
refresh_token: Set(Some(refresh_token_to_save.clone())),
|
||||
is_init: Set(true), // 新用户默认未初始化
|
||||
..Default::default()
|
||||
}
|
||||
|
||||
@@ -47,18 +47,3 @@ pub struct LoginResp {
|
||||
pub uid: String,
|
||||
pub expire: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RefreshTokenReq {
|
||||
pub refresh_token: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RefreshTokenResp {
|
||||
pub token: String,
|
||||
pub refresh_token: String,
|
||||
pub expire: String,
|
||||
pub uid: String,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user