use crate::KenAuthError; use crate::models::users::users; use crate::protos::auth::v1::*; use chrono::Utc; use rand::RngExt as _; use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set}; use crate::Accounts; use crate::errors::AuthError; use crate::share::{auth_error as error, auth_store as store}; fn bot_public(user: &users::Model) -> crate::protos::auth::common::v1::UserPublic { crate::share::auth_store::user_public(user, String::new(), None) } impl Accounts { #[tracing::instrument(skip_all, fields(request_id = %envelope::context::resolve_request_id(request_id)))] pub(crate) async fn get_user_public_inner( &self, request_id: Option<&str>, payload: GetUserPublicRequest, ) -> Result { let reference = payload .user .as_ref() .ok_or_else(|| error::status(&AuthError::MalformedRequest, request_id))?; let user = store::find_user(self.db(), reference) .await .map_err(|error_value| error::status(&error_value, request_id))? .ok_or_else(|| error::status(&AuthError::UserNotFound, request_id))?; let email = user.email.clone(); Ok(GetUserPublicResponse { user: Some(store::user_public(&user, email, None)), }) } #[tracing::instrument(skip_all, fields(request_id = %envelope::context::resolve_request_id(request_id)))] pub(crate) async fn batch_get_user_public_inner( &self, request_id: Option<&str>, payload: BatchGetUserPublicRequest, ) -> Result { let ids = payload.user_ids; if ids.is_empty() { return Ok(BatchGetUserPublicResponse { users: Vec::new() }); } if ids.len() > store::MAX_BATCH_USER_IDS { return Err(error::invalid_argument( format!( "at most {} user ids can be fetched at once", store::MAX_BATCH_USER_IDS ), request_id, )); } let mut unique: Vec = Vec::with_capacity(ids.len()); for id in ids { if !unique.contains(&id) { unique.push(id); } } let rows = users::Entity::find() .filter(users::Column::Id.is_in(unique.iter().copied())) .all(self.db().read()) .await .map_err(|error_value| error::status(&AuthError::from(error_value), request_id))?; let mut users = Vec::with_capacity(unique.len()); for user in &rows { users.push(store::user_public(user, user.email.clone(), None)); } Ok(BatchGetUserPublicResponse { users }) } #[tracing::instrument(skip_all, fields(request_id = %envelope::context::resolve_request_id(request_id)))] pub(crate) async fn get_user_status_inner( &self, request_id: Option<&str>, payload: GetUserStatusRequest, ) -> Result { let reference = payload .user .as_ref() .ok_or_else(|| error::status(&AuthError::MalformedRequest, request_id))?; let user = store::find_user(self.db(), reference) .await .map_err(|error_value| error::status(&error_value, request_id))?; let Some(user) = user else { return Ok(GetUserStatusResponse { status: Some(store::empty_status()), }); }; let mfa = store::active_mfa_count(self.db(), user.id) .await .map_err(|error_value| error::status(&error_value, request_id))?; Ok(GetUserStatusResponse { status: Some(store::user_status_of(&user, mfa)), }) } #[tracing::instrument(skip_all, fields(request_id = %envelope::context::resolve_request_id(request_id)))] pub(crate) async fn ensure_bot_user_inner( &self, request_id: Option<&str>, payload: EnsureBotUserRequest, ) -> Result { let username = payload.username.trim(); let lower = username.to_lowercase(); if lower.is_empty() || lower.len() > 32 || !lower .chars() .all(|value| value.is_ascii_alphanumeric() || value == '-' || value == '_') { return Err(error::status(&AuthError::InvalidUsername, request_id)); } let db = self.db().write(); if let Some(existing) = users::Entity::find() .filter(users::Column::Lowername.eq(&lower)) .one(db) .await .map_err(|value| error::status(&AuthError::from(value), request_id))? { if !existing.bot { return Err(error::status(&AuthError::UsernameTaken, request_id)); } return Ok(EnsureBotUserResponse { user: Some(bot_public(&existing)), created: false, }); } let displayname = if payload.displayname.trim().is_empty() { username.to_string() } else { payload.displayname.trim().to_string() }; let now = Utc::now(); let password_hash = crate::share::password::hash(format!("bot-{lower}-{now}"), request_id) .await .map_err(|value| error::status(&value, request_id))?; let inserted = users::ActiveModel { id: Set({ let mut rng = rand::rng(); let digits = rng.random_range(5u32..=10); let upper = 10i64.pow(digits); rng.random_range((upper / 10)..upper) }), username: Set(username.to_string()), lowername: Set(lower.clone()), displayname: Set(Some(displayname)), email: Set(format!("{lower}@bots.invalid")), email_verified_at: Set(Some(now)), password_hash: Set(password_hash), password_updated_at: Set(Some(now)), allow_use: Set(true), forbid_login: Set(true), forbid_login_reason: Set(Some("bot account".to_string())), bot: Set(true), system: Set(payload.system), created_at: Set(now), ..Default::default() } .insert(db) .await; match inserted { Ok(user) => Ok(EnsureBotUserResponse { user: Some(bot_public(&user)), created: true, }), Err(failure) => { // 只有唯一约束冲突才能走「已存在」分支;其余数据库错误必须原样上抛。 if !matches!( failure.sql_err(), Some(sea_orm::SqlErr::UniqueConstraintViolation(_)) ) { return Err(error::status(&AuthError::from(failure), request_id)); } let existing = users::Entity::find() .filter(users::Column::Lowername.eq(&lower)) .one(db) .await .map_err(|value| error::status(&AuthError::from(value), request_id))? .ok_or_else(|| error::status(&AuthError::UsernameTaken, request_id))?; if !existing.bot { return Err(error::status(&AuthError::UsernameTaken, request_id)); } Ok(EnsureBotUserResponse { user: Some(bot_public(&existing)), created: false, }) } } } }