590c07b
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<i64> {
let user_id = session.user_id()?.ok_or(AuthError::Unauthenticated)?;
let version = session
.security_version()?
.ok_or(AuthError::Unauthenticated)?;
let id = session
.get::<String>("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::Model> {
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<Vec<users::Model>> {
let mut unique: Vec<i64> = 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<users::Model> {
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)
}
}