main
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<GetUserPublicResponse, KenAuthError> {
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<BatchGetUserPublicResponse, KenAuthError> {
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<i64> = 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<GetUserStatusResponse, KenAuthError> {
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<EnsureBotUserResponse, KenAuthError> {
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,
})
}
}
}
}