185 lines
6.4 KiB
Rust
185 lines
6.4 KiB
Rust
use axum::http::{HeaderMap, HeaderValue, StatusCode, header::SET_COOKIE};
|
||
use module_auth::{
|
||
AuthLoginMethod, AuthUser, CreateRefreshSessionInput, LogoutError, RefreshSessionError,
|
||
};
|
||
use platform_auth::{
|
||
AccessTokenClaims, AccessTokenClaimsInput, AuthProvider, BindingStatus,
|
||
build_refresh_session_clear_cookie, build_refresh_session_set_cookie,
|
||
create_refresh_session_token, hash_refresh_session_token, sign_access_token,
|
||
};
|
||
use time::OffsetDateTime;
|
||
|
||
use crate::session_client::SessionClientContext;
|
||
use crate::{
|
||
http_error::AppError, request_context::RequestContext, state::AppState,
|
||
tracking::record_daily_login_tracking_event_after_success as record_daily_login_tracking_event_via_unified_path,
|
||
};
|
||
|
||
#[derive(Debug, Clone)]
|
||
pub struct SignedAuthSession {
|
||
pub access_token: String,
|
||
pub refresh_token: String,
|
||
}
|
||
|
||
pub fn create_password_auth_session(
|
||
state: &AppState,
|
||
user: &AuthUser,
|
||
session_client: &SessionClientContext,
|
||
) -> Result<SignedAuthSession, AppError> {
|
||
create_auth_session(state, user, session_client, AuthLoginMethod::Password)
|
||
}
|
||
|
||
#[cfg(not(test))]
|
||
pub async fn record_daily_login_tracking_event_after_auth_success(
|
||
state: &AppState,
|
||
request_context: &RequestContext,
|
||
user_id: &str,
|
||
login_method: AuthLoginMethod,
|
||
) {
|
||
// 登录埋点是运营数据,不应反向阻断已经成功的认证会话签发;每日登录也走统一埋点 helper/procedure。
|
||
record_daily_login_tracking_event_via_unified_path(
|
||
state,
|
||
request_context,
|
||
user_id,
|
||
login_method,
|
||
)
|
||
.await;
|
||
}
|
||
|
||
#[cfg(test)]
|
||
pub async fn record_daily_login_tracking_event_after_auth_success(
|
||
_state: &AppState,
|
||
_request_context: &RequestContext,
|
||
_user_id: &str,
|
||
_login_method: AuthLoginMethod,
|
||
) {
|
||
// 单元测试默认不启动 SpacetimeDB;这里仅验证登录链路调用点能通过编译并保持非阻断语义。
|
||
}
|
||
|
||
pub fn create_auth_session(
|
||
state: &AppState,
|
||
user: &AuthUser,
|
||
session_client: &SessionClientContext,
|
||
session_provider: AuthLoginMethod,
|
||
) -> Result<SignedAuthSession, AppError> {
|
||
let refresh_token = create_refresh_session_token();
|
||
let refresh_token_hash = hash_refresh_session_token(&refresh_token);
|
||
let session = state
|
||
.refresh_session_service()
|
||
.create_session(
|
||
CreateRefreshSessionInput {
|
||
user_id: user.id.clone(),
|
||
refresh_token_hash,
|
||
issued_by_provider: session_provider.clone(),
|
||
client_info: session_client.to_refresh_session_client_info(),
|
||
},
|
||
OffsetDateTime::now_utc(),
|
||
)
|
||
.map_err(map_refresh_session_error)?;
|
||
let access_token = sign_access_token_for_user(
|
||
state,
|
||
user,
|
||
&session.session.session_id,
|
||
Some(&session_provider),
|
||
)?;
|
||
|
||
Ok(SignedAuthSession {
|
||
access_token,
|
||
refresh_token,
|
||
})
|
||
}
|
||
|
||
pub fn sign_access_token_for_user(
|
||
state: &AppState,
|
||
user: &AuthUser,
|
||
session_id: &str,
|
||
session_provider_override: Option<&AuthLoginMethod>,
|
||
) -> Result<String, AppError> {
|
||
let access_claims = AccessTokenClaims::from_input(
|
||
AccessTokenClaimsInput {
|
||
user_id: user.id.clone(),
|
||
session_id: session_id.to_string(),
|
||
provider: map_auth_provider(session_provider_override.unwrap_or(&user.login_method)),
|
||
roles: vec!["user".to_string()],
|
||
token_version: user.token_version,
|
||
phone_verified: user.phone_number_masked.is_some(),
|
||
binding_status: map_binding_status(&user.binding_status),
|
||
display_name: Some(user.display_name.clone()),
|
||
},
|
||
state.auth_jwt_config(),
|
||
OffsetDateTime::now_utc(),
|
||
)
|
||
.map_err(|error| {
|
||
AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_message(error.to_string())
|
||
})?;
|
||
|
||
sign_access_token(&access_claims, state.auth_jwt_config()).map_err(|error| {
|
||
AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_message(error.to_string())
|
||
})
|
||
}
|
||
|
||
pub fn build_refresh_session_cookie_header(
|
||
state: &AppState,
|
||
refresh_token: &str,
|
||
) -> Result<HeaderValue, AppError> {
|
||
let refresh_cookie =
|
||
build_refresh_session_set_cookie(refresh_token, state.refresh_cookie_config());
|
||
HeaderValue::from_str(&refresh_cookie).map_err(|error| {
|
||
AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR)
|
||
.with_message(format!("refresh cookie 头构造失败:{error}"))
|
||
})
|
||
}
|
||
|
||
pub fn build_clear_refresh_session_cookie_header(
|
||
state: &AppState,
|
||
) -> Result<HeaderValue, AppError> {
|
||
let refresh_cookie = build_refresh_session_clear_cookie(state.refresh_cookie_config());
|
||
HeaderValue::from_str(&refresh_cookie).map_err(|error| {
|
||
AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR)
|
||
.with_message(format!("refresh cookie 头构造失败:{error}"))
|
||
})
|
||
}
|
||
|
||
pub fn attach_set_cookie_header(headers: &mut HeaderMap, set_cookie: HeaderValue) {
|
||
headers.insert(SET_COOKIE, set_cookie);
|
||
}
|
||
|
||
pub fn map_refresh_session_error(error: RefreshSessionError) -> AppError {
|
||
match error {
|
||
RefreshSessionError::MissingToken
|
||
| RefreshSessionError::SessionNotFound
|
||
| RefreshSessionError::SessionExpired
|
||
| RefreshSessionError::UserNotFound => {
|
||
AppError::from_status(StatusCode::UNAUTHORIZED).with_message(error.to_string())
|
||
}
|
||
RefreshSessionError::Store(message) => {
|
||
AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_message(message)
|
||
}
|
||
}
|
||
}
|
||
|
||
pub fn map_logout_error(error: LogoutError) -> AppError {
|
||
match error {
|
||
LogoutError::UserNotFound => AppError::from_status(StatusCode::UNAUTHORIZED)
|
||
.with_message("当前登录态已失效,请重新登录"),
|
||
LogoutError::Store(message) => {
|
||
AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_message(message)
|
||
}
|
||
}
|
||
}
|
||
|
||
fn map_auth_provider(login_method: &AuthLoginMethod) -> AuthProvider {
|
||
match login_method {
|
||
AuthLoginMethod::Password => AuthProvider::Password,
|
||
AuthLoginMethod::Phone => AuthProvider::Phone,
|
||
AuthLoginMethod::Wechat => AuthProvider::Wechat,
|
||
}
|
||
}
|
||
|
||
fn map_binding_status(binding_status: &module_auth::AuthBindingStatus) -> BindingStatus {
|
||
match binding_status {
|
||
module_auth::AuthBindingStatus::Active => BindingStatus::Active,
|
||
module_auth::AuthBindingStatus::PendingBindPhone => BindingStatus::PendingBindPhone,
|
||
}
|
||
}
|