Skip to content
Clone
b453d5d
258 lines 9.9 KB Rust
use models::users::user::ProfileVisibility;
use models::users::{user, user_profiles};
use sea_orm::{
ActiveModelTrait, ColumnTrait, EntityTrait, IntoActiveModel, QueryFilter, Set, TransactionTrait,
};
use serde::{Deserialize, Serialize};
use time::{Date, OffsetDateTime};
use super::AuthService;
use crate::errors::AuthError;
use crate::share::types::UserPublic;
use crate::share::validate::validate_age;
#[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>), AuthError> {
let db = self.db();
let Some(user) = user::Entity::find_by_id(uid).one(db.read()).await? else {
return Err(AuthError::NotFound);
};
let profile = user_profiles::Entity::find_by_id(uid)
.one(db.read())
.await?;
let me = UserMeDto {
user_id: user.uid,
username: user.username,
display_name: user.display_name,
avatar_url: space::services::spaces::resolve_user_avatar_url(
user.uid,
&user.avatar_url,
),
is_active: user.is_active,
is_restricted: user.is_restricted,
prohibit_login: user.prohibit_login,
visibility: user.visibility,
bot: user.bot,
system: user.system,
timezone: user.timezone,
locale: user.locale,
language: user.language,
created_at: user.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<(), AuthError> {
let db = self.db();
let Some(user) = user::Entity::find_by_id(uid).one(db.write()).await? else {
return Err(AuthError::NotFound);
};
let now = OffsetDateTime::now_utc();
let mut user_active = user.into_active_model();
if let Some(display_name) = cmd.display_name.filter(|name| !name.trim().is_empty()) {
let display_name = display_name.trim().to_string();
if display_name.len() > 64 {
return Err(AuthError::BadRequest(
"display_name must be at most 64 characters".to_string(),
));
}
user_active.display_name = Set(display_name);
}
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_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(AuthError::from)
}
pub async fn update_username(
&self,
uid: i64,
new_username: &str,
) -> Result<UserPublic, AuthError> {
let username = crate::share::validate::validate_username(new_username.trim())?;
let lower = username.to_lowercase();
let db = self.db();
let user = user::Entity::find_by_id(uid)
.one(db.read())
.await?
.ok_or(AuthError::NotFound)?;
if user.username != username {
let exists = user::Entity::find()
.filter(user::Column::LowerName.eq(&lower))
.one(db.read())
.await?;
if exists.is_some() {
return Err(AuthError::Conflict("username already taken".to_string()));
}
let mut active = user.clone().into_active_model();
active.username = Set(username.clone());
active.lower_name = Set(lower);
active.updated_at = Set(OffsetDateTime::now_utc());
active.update(db.write()).await?;
}
Ok(UserPublic {
user_id: uid,
username: username.clone(),
display_name: user.display_name.clone(),
avatar_url: space::services::spaces::resolve_user_avatar_url(uid, &user.avatar_url),
})
}
}