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, pub locale: Option, pub language: Option, pub created_at: OffsetDateTime, } #[derive(Debug, Clone, Serialize, Deserialize, Default, utoipa::ToSchema)] pub struct ProfileDto { pub real_name: Option, pub bio: Option, pub pronouns: Option, pub website: Option, pub location: Option, pub company: Option, pub date_of_birth: Option, pub show_email: bool, pub show_location: bool, pub show_real_name: bool, pub show_activity: bool, pub privacy_agreed_at: Option, } #[derive(Debug, Clone, Serialize, Deserialize, Default, utoipa::ToSchema)] pub struct UpdateProfileCmd { pub display_name: Option, pub locale: Option, pub language: Option, pub timezone: Option, pub real_name: Option, pub bio: Option, pub pronouns: Option, pub website: Option, pub location: Option, pub company: Option, pub date_of_birth: Option, pub show_email: Option, pub show_location: Option, pub show_real_name: Option, pub show_activity: Option, #[serde(default)] pub visibility: Option, } impl AuthService { pub async fn me(&self, uid: i64) -> Result<(UserMeDto, Option), 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 { 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), }) } }