feat: add multi-device session identity
This commit is contained in:
113
server-rs/crates/api-server/src/auth_sessions.rs
Normal file
113
server-rs/crates/api-server/src/auth_sessions.rs
Normal file
@@ -0,0 +1,113 @@
|
||||
use axum::{
|
||||
Json,
|
||||
extract::{Extension, State},
|
||||
http::StatusCode,
|
||||
};
|
||||
use platform_auth::hash_refresh_session_token;
|
||||
use serde::Serialize;
|
||||
use time::OffsetDateTime;
|
||||
|
||||
use crate::{
|
||||
api_response::json_success_body,
|
||||
auth::{AuthenticatedAccessToken, RefreshSessionToken},
|
||||
http_error::AppError,
|
||||
request_context::RequestContext,
|
||||
session_client::mask_ip,
|
||||
state::AppState,
|
||||
};
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AuthSessionsResponse {
|
||||
pub sessions: Vec<AuthSessionSummaryPayload>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AuthSessionSummaryPayload {
|
||||
pub session_id: String,
|
||||
pub client_type: String,
|
||||
pub client_runtime: String,
|
||||
pub client_platform: String,
|
||||
pub client_label: String,
|
||||
pub device_display_name: String,
|
||||
pub mini_program_app_id: Option<String>,
|
||||
pub mini_program_env: Option<String>,
|
||||
pub user_agent: Option<String>,
|
||||
pub ip_masked: Option<String>,
|
||||
pub is_current: bool,
|
||||
pub created_at: String,
|
||||
pub last_seen_at: String,
|
||||
pub expires_at: String,
|
||||
}
|
||||
|
||||
pub async fn auth_sessions(
|
||||
State(state): State<AppState>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||||
maybe_refresh_token: Option<Extension<RefreshSessionToken>>,
|
||||
) -> Result<Json<serde_json::Value>, AppError> {
|
||||
// 当前设备识别仍然依赖 refresh cookie 命中的原始 token,对旧前端行为保持兼容。
|
||||
let user_id = authenticated.claims().user_id().to_string();
|
||||
let current_refresh_token_hash = maybe_refresh_token.and_then(|token| {
|
||||
let token = token.0.token().trim();
|
||||
if token.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(hash_refresh_session_token(token))
|
||||
});
|
||||
|
||||
let sessions = state
|
||||
.refresh_session_service()
|
||||
.list_active_sessions_by_user(&user_id, OffsetDateTime::now_utc())
|
||||
.map_err(map_refresh_session_list_error)?;
|
||||
|
||||
Ok(json_success_body(
|
||||
Some(&request_context),
|
||||
AuthSessionsResponse {
|
||||
sessions: sessions
|
||||
.sessions
|
||||
.into_iter()
|
||||
.map(|session| {
|
||||
let is_current = current_refresh_token_hash.as_ref().is_some_and(|hash| {
|
||||
session.refresh_token_hash == *hash
|
||||
});
|
||||
let client_label = session.client_info.device_display_name.clone();
|
||||
|
||||
AuthSessionSummaryPayload {
|
||||
session_id: session.session_id,
|
||||
client_type: session.client_info.client_type,
|
||||
client_runtime: session.client_info.client_runtime,
|
||||
client_platform: session.client_info.client_platform,
|
||||
client_label,
|
||||
device_display_name: session.client_info.device_display_name,
|
||||
mini_program_app_id: session.client_info.mini_program_app_id,
|
||||
mini_program_env: session.client_info.mini_program_env,
|
||||
user_agent: session.client_info.user_agent,
|
||||
ip_masked: mask_ip(session.client_info.ip.as_deref()),
|
||||
is_current,
|
||||
created_at: session.created_at,
|
||||
last_seen_at: session.last_seen_at,
|
||||
expires_at: session.expires_at,
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
fn map_refresh_session_list_error(error: module_auth::RefreshSessionError) -> AppError {
|
||||
match error {
|
||||
module_auth::RefreshSessionError::UserNotFound => AppError::from_status(StatusCode::UNAUTHORIZED)
|
||||
.with_message("当前登录态已失效,请重新登录"),
|
||||
module_auth::RefreshSessionError::MissingToken
|
||||
| module_auth::RefreshSessionError::SessionNotFound
|
||||
| module_auth::RefreshSessionError::SessionExpired => {
|
||||
AppError::from_status(StatusCode::UNAUTHORIZED).with_message(error.to_string())
|
||||
}
|
||||
module_auth::RefreshSessionError::Store(message) => {
|
||||
AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_message(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user