use crate::models::users::{user_sessions, users}; use crate::session::Session; use chrono::Utc; use sea_orm::{ColumnTrait, EntityTrait, IntoActiveModel, QueryFilter, Set}; use uuid::Uuid; use crate::share::user as store; use crate::{Accounts, AuthError, AuthResult}; use crate::services::authority::session_token_hash; impl Accounts { #[tracing::instrument(skip_all, fields(user_id = user_id, session_id = %session_id, request_id = %envelope::context::resolve_request_id(request_id)))] pub async fn check_session( &self, user_id: i64, version: i32, session_id: &str, request_id: Option<&str>, ) -> AuthResult<(users::Model, bool)> { // security_version 判定一律读主库:副本延迟会让刚吊销的旧版本短暂放行。 let user = users::Entity::find_by_id(user_id) .one(self.db().write()) .await? .ok_or(AuthError::Unauthenticated)?; if !{ let user = &user; user.id > 0 && user.deleted_at.is_none() && user.allow_use && (!user.forbid_login || user .endtime_forbid_login_at .is_none_or(|until| until > Utc::now())) } { return Err(AuthError::Unauthenticated); } if version <= 0 || version != user.security_version { return Err(AuthError::Unauthenticated); } let id = Uuid::parse_str(session_id).map_err(|_| AuthError::Unauthenticated)?; let row = user_sessions::Entity::find_by_id(id) .one(self.db().write()) .await? .ok_or(AuthError::Unauthenticated)?; // `token_hash` 是会话凭据的摘要:只有拿着明文会话 ID 的一方能算出来。 // 不校验它,单有数据库读权限的人就能直接拿行里的 ID 复会话。 if !crate::share::crypto::constant_time_eq( row.token_hash.as_bytes(), session_token_hash(id).as_bytes(), ) { return Err(AuthError::Unauthenticated); } if row.user_id != user_id || row.security_version != version || row.revoked_at.is_some() || row.expires_at <= Utc::now() { return Err(AuthError::Unauthenticated); } Ok((user, row.mfa_verified_at.is_some())) } #[tracing::instrument(skip_all, fields(request_id = %envelope::context::resolve_request_id(request_id)))] pub async fn require_session( &self, session: &Session, request_id: Option<&str>, ) -> AuthResult { let user_id = session.user_id()?.ok_or(AuthError::Unauthenticated)?; let version = session .security_version()? .ok_or(AuthError::Unauthenticated)?; let id = session .get::("auth_session_id")? .ok_or(AuthError::Unauthenticated)?; let _ = self .check_session(user_id, version, &id, request_id) .await?; Ok(user_id) } pub async fn user(&self, user_id: i64) -> AuthResult { users::Entity::find_by_id(user_id) .one(self.db().read()) .await? .ok_or(AuthError::UserNotFound) } pub async fn users(&self, user_ids: &[i64]) -> AuthResult> { let mut unique: Vec = Vec::with_capacity(user_ids.len()); for user_id in user_ids.iter().copied().filter(|user_id| *user_id > 0) { if !unique.contains(&user_id) { unique.push(user_id); } } if unique.is_empty() { return Ok(Vec::new()); } Ok(users::Entity::find() .filter(users::Column::Id.is_in(unique)) .all(self.db().read()) .await?) } pub async fn update(&self, user_id: i64, patch: serde_json::Value) -> AuthResult { let user = store::load_active_user(self.db(), user_id).await?; let version = patch .get("version") .and_then(|value| value.as_i64()) .ok_or(AuthError::MalformedRequest)?; if i64::from(user.version) != version { return Err(AuthError::VersionMismatch { current: user.version, }); } let mut active = user.clone().into_active_model(); if let Some(name) = patch.get("username").and_then(|value| value.as_str()) { let name = crate::share::validate::validate_username(name) .map_err(|_| AuthError::InvalidUsername)?; active.lowername = Set(name.to_lowercase()); active.username = Set(name); } if let Some(value) = patch.get("displayname") { active.displayname = Set(match value.as_str() { Some(value) => Some( crate::share::validate::validate_display_name(value) .map_err(|_| AuthError::InvalidDisplayname)?, ), None if value.is_null() => None, _ => return Err(AuthError::MalformedRequest), }); } if let Some(value) = patch.get("public_show_email") { active.public_show_email = Set(value.as_bool().ok_or(AuthError::MalformedRequest)?); } active.version = Set(user .version .checked_add(1) .ok_or(AuthError::VersionMismatch { current: user.version, })?); active.updated_at = Set(Utc::now()); let updated = users::Entity::update_many() .set(active) .filter(users::Column::Id.eq(user_id)) .filter(users::Column::Version.eq(user.version)) .exec_with_returning(self.db().write()) .await? .pop() .ok_or(AuthError::VersionMismatch { current: user.version, })?; crate::services::me::invalidate_me_cache(self.cache(), user_id).await; Ok(updated) } }