Skip to content
Clone
163952e
340 lines 12.9 KB Rust
use models::users::user::ProfileVisibility;
use models::users::{user, user_profiles};
use sea_orm::{ActiveModelTrait, EntityTrait, IntoActiveModel, Set, TransactionTrait};
use serde::{Deserialize, Serialize};
use time::{Date, OffsetDateTime};
use crate::api_error::AuthBusinessError;
use crate::service::AuthService;
use crate::share::account::types::UserPublic;
use crate::share::account::validate;
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct UserMeDto {
pub user_id: i64,
pub username: String,
pub display_name: String,
pub avatar_url: String,
pub is_active: bool,
pub is_restricted: bool,
pub prohibit_login: bool,
pub visibility: ProfileVisibility,
pub bot: bool,
pub system: bool,
pub timezone: Option<String>,
pub locale: Option<String>,
pub language: Option<String>,
pub created_at: OffsetDateTime,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, utoipa::ToSchema)]
pub struct ProfileDto {
pub real_name: Option<String>,
pub bio: Option<String>,
pub pronouns: Option<String>,
pub website: Option<String>,
pub location: Option<String>,
pub company: Option<String>,
pub date_of_birth: Option<Date>,
pub show_email: bool,
pub show_location: bool,
pub show_real_name: bool,
pub show_activity: bool,
pub privacy_agreed_at: Option<OffsetDateTime>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, utoipa::ToSchema)]
pub struct UpdateProfileCmd {
pub display_name: Option<String>,
pub locale: Option<String>,
pub language: Option<String>,
pub timezone: Option<String>,
pub real_name: Option<String>,
pub bio: Option<String>,
pub pronouns: Option<String>,
pub website: Option<String>,
pub location: Option<String>,
pub company: Option<String>,
pub date_of_birth: Option<Date>,
pub show_email: Option<bool>,
pub show_location: Option<bool>,
pub show_real_name: Option<bool>,
pub show_activity: Option<bool>,
#[serde(default)]
pub visibility: Option<ProfileVisibility>,
}
impl AuthService {
pub async fn me(&self, uid: i64) -> Result<(UserMeDto, Option<ProfileDto>), AuthBusinessError> {
export::users::ensure_local_user(&self.state, uid).await?;
let db = self.db();
let Some(user) = user::Entity::find_by_id(uid).one(db.read()).await? else {
return Err(AuthBusinessError::NotFound);
};
let client = self.state.kenauth.as_ref().ok_or_else(|| {
AuthBusinessError::Internal("auth service is not configured".to_string())
})?;
let authority = client.get_authority_user(uid).await.map_err(map_kenauth)?;
let public = client.get_user_public(uid).await.map_err(map_kenauth)?;
let status = client.get_user_status(uid).await.map_err(map_kenauth)?;
let display_name = if public.displayname.trim().is_empty() {
public.username.clone()
} else {
public.displayname.clone()
};
let avatar_raw = if user.local_avatar_url.is_empty() {
public.avatar_url.clone()
} else {
user.local_avatar_url.clone()
};
let created_at = authority
.created_at
.as_ref()
.and_then(|value| {
OffsetDateTime::from_unix_timestamp(value.seconds)
.ok()
.and_then(|dt| {
dt.replace_nanosecond(value.nanos.clamp(0, 999_999_999) as u32)
.ok()
})
})
.unwrap_or_else(OffsetDateTime::now_utc);
let profile = user_profiles::Entity::find_by_id(uid)
.one(db.read())
.await?;
let me = UserMeDto {
user_id: uid,
username: public.username,
display_name,
avatar_url: export::urls::resolve_user_avatar_url(uid, &avatar_raw),
is_active: status.allow_use && status.deleted_at.is_none(),
is_restricted: user.is_restricted,
prohibit_login: authority.forbid_login,
visibility: user.visibility,
bot: public.bot,
system: public.system,
timezone: user.timezone,
locale: user.locale,
language: user.language,
created_at,
};
let profile = profile.map(|row| ProfileDto {
real_name: row.real_name,
bio: row.bio,
pronouns: row.pronouns,
website: row.website,
location: row.location,
company: row.company,
date_of_birth: row.date_of_birth,
show_email: row.show_email,
show_location: row.show_location,
show_real_name: row.show_real_name,
show_activity: row.show_activity,
privacy_agreed_at: row.privacy_agreed_at,
});
Ok((me, profile))
}
pub async fn update_profile(
&self,
uid: i64,
cmd: UpdateProfileCmd,
) -> Result<(), AuthBusinessError> {
export::users::ensure_local_user(&self.state, uid).await?;
let db = self.db();
let Some(user) = user::Entity::find_by_id(uid).one(db.write()).await? else {
return Err(AuthBusinessError::NotFound);
};
let now = OffsetDateTime::now_utc();
let mut user_active = user.into_active_model();
if let Some(display_name) = cmd
.display_name
.as_deref()
.map(str::trim)
.filter(|name| !name.is_empty())
{
if display_name.chars().count() > 64 {
return Err(AuthBusinessError::BadRequest(
"display_name must be at most 64 characters".to_string(),
));
}
let client = self.state.kenauth.as_ref().ok_or_else(|| {
AuthBusinessError::Internal("auth service is not configured".to_string())
})?;
let current = client.get_authority_user(uid).await.map_err(map_kenauth)?;
client
.update_authority_user(
uid,
None,
Some(display_name.to_string()),
false,
None,
current.version,
)
.await
.map_err(map_kenauth)?;
}
if let Some(locale) = cmd.locale {
user_active.locale = Set(Some(locale));
}
if let Some(language) = cmd.language {
user_active.language = Set(Some(language));
}
if let Some(timezone) = cmd.timezone {
user_active.timezone = Set(Some(timezone));
}
if let Some(dob) = cmd.date_of_birth {
validate::validate_age(Some(dob), self.config().auth.min_age)?;
}
if let Some(visibility) = cmd.visibility {
user_active.visibility = Set(visibility);
}
user_active.updated_at = Set(now);
TransactionTrait::transaction(db.primary(), |txn| {
Box::pin(async move {
user_active.update(txn).await?;
let existing = user_profiles::Entity::find_by_id(uid).one(txn).await?;
if let Some(row) = existing {
let mut active = row.into_active_model();
if let Some(value) = cmd.real_name {
active.real_name = Set(Some(value));
}
if let Some(value) = cmd.bio {
active.bio = Set(Some(value));
}
if let Some(value) = cmd.pronouns {
active.pronouns = Set(Some(value));
}
if let Some(value) = cmd.website {
active.website = Set(Some(value));
}
if let Some(value) = cmd.location {
active.location = Set(Some(value));
}
if let Some(value) = cmd.company {
active.company = Set(Some(value));
}
if let Some(value) = cmd.date_of_birth {
active.date_of_birth = Set(Some(value));
}
if let Some(value) = cmd.show_email {
active.show_email = Set(value);
}
if let Some(value) = cmd.show_location {
active.show_location = Set(value);
}
if let Some(value) = cmd.show_real_name {
active.show_real_name = Set(value);
}
if let Some(value) = cmd.show_activity {
active.show_activity = Set(value);
}
active.updated_at = Set(now);
active.update(txn).await?;
} else {
let row = user_profiles::ActiveModel {
user_id: Set(uid),
real_name: Set(cmd.real_name),
bio: Set(cmd.bio),
pronouns: Set(cmd.pronouns),
website: Set(cmd.website),
location: Set(cmd.location),
company: Set(cmd.company),
pronouns_privacy: Set(None),
date_of_birth: Set(cmd.date_of_birth),
banner_url: Set(None),
banner_hash: Set(None),
banner_color: Set(None),
accent_color: Set(None),
timezone_privacy_flags: Set(None),
custom_status_emoji: Set(None),
custom_status_text: Set(None),
custom_status_expires_at: Set(None),
show_email: Set(cmd.show_email.unwrap_or(false)),
show_location: Set(cmd.show_location.unwrap_or(false)),
show_real_name: Set(cmd.show_real_name.unwrap_or(false)),
show_activity: Set(cmd.show_activity.unwrap_or(false)),
privacy_agreed_at: Set(None),
created_at: Set(now),
updated_at: Set(now),
};
row.insert(txn).await?;
}
Ok::<(), sea_orm::DbErr>(())
})
})
.await
.map_err(AuthBusinessError::from)
}
pub async fn update_username(
&self,
uid: i64,
new_username: &str,
) -> Result<UserPublic, AuthBusinessError> {
let username = crate::share::account::validate::validate_username(new_username.trim())?;
let db = self.db();
let local = user::Entity::find_by_id(uid).one(db.read()).await?;
let client = self.state.kenauth.as_ref().ok_or_else(|| {
AuthBusinessError::Internal("auth service is not configured".to_string())
})?;
let current = client.get_authority_user(uid).await.map_err(map_kenauth)?;
let updated = if current.username != username {
client
.update_authority_user(
uid,
Some(username.clone()),
None,
false,
None,
current.version,
)
.await
.map_err(map_kenauth)?
} else {
current
};
let public = client.get_user_public(uid).await.map_err(map_kenauth)?;
let display_name = updated
.displayname
.clone()
.filter(|value| !value.trim().is_empty())
.unwrap_or_else(|| updated.username.clone());
let avatar_raw = local
.as_ref()
.map(|row| row.local_avatar_url.as_str())
.filter(|value| !value.is_empty())
.unwrap_or(public.avatar_url.as_str());
Ok(UserPublic {
user_id: uid,
username: updated.username,
display_name,
avatar_url: export::urls::resolve_user_avatar_url(uid, avatar_raw),
})
}
}
fn map_kenauth(error: crate::auth_api_error::KenAuthError) -> AuthBusinessError {
use crate::auth_api_error::KenAuthError;
match error {
KenAuthError::NotFound => AuthBusinessError::NotFound,
KenAuthError::Unauthenticated => AuthBusinessError::Unauthorized,
KenAuthError::Rejected(message) => {
let lower = message.to_lowercase();
if lower.contains("conflict") || lower.contains("taken") || lower.contains("already") {
AuthBusinessError::Conflict(message)
} else {
AuthBusinessError::BadRequest(message)
}
}
KenAuthError::Unavailable(message)
| KenAuthError::Config(message)
| KenAuthError::Internal(message) => AuthBusinessError::Internal(message),
}
}