use serde::{Deserialize, Serialize}; use crate::error::AppError; use crate::models::users::UserProfile; use crate::service::UserService; use crate::session::Session; use super::util::merge_optional_text; #[derive(Deserialize, Serialize, Clone, Debug, utoipa::ToSchema)] pub struct UpdateUserProfileParams { pub full_name: Option, pub company: Option, pub location: Option, pub website_url: Option, pub twitter_username: Option, pub timezone: Option, pub language: Option, pub profile_readme: Option, } impl UserService { pub async fn user_profile(&self, ctx: &Session) -> Result { let user_uid = ctx.user().ok_or(AppError::Unauthorized)?; self.ensure_user_profile(user_uid).await } pub async fn user_update_profile( &self, ctx: &Session, params: UpdateUserProfileParams, ) -> Result { let user_uid = ctx.user().ok_or(AppError::Unauthorized)?; let current = self.ensure_user_profile(user_uid).await?; let now = chrono::Utc::now(); sqlx::query_as::<_, UserProfile>( "UPDATE user_profile SET full_name = $1, company = $2, location = $3, website_url = $4, \ twitter_username = $5, timezone = $6, language = $7, profile_readme = $8, updated_at = $9 \ WHERE user_id = $10 RETURNING user_id, full_name, company, location, website_url, \ twitter_username, timezone, language, profile_readme, created_at, updated_at", ) .bind(merge_optional_text(params.full_name, current.full_name)) .bind(merge_optional_text(params.company, current.company)) .bind(merge_optional_text(params.location, current.location)) .bind(merge_optional_text(params.website_url, current.website_url)) .bind(merge_optional_text(params.twitter_username, current.twitter_username)) .bind(merge_optional_text(params.timezone, current.timezone)) .bind(merge_optional_text(params.language, current.language)) .bind(merge_optional_text(params.profile_readme, current.profile_readme)) .bind(now) .bind(user_uid) .fetch_one(self.ctx.db.writer()) .await .map_err(AppError::Database) } async fn ensure_user_profile(&self, user_uid: uuid::Uuid) -> Result { if let Some(profile) = self.find_user_profile(user_uid).await? { return Ok(profile); } let now = chrono::Utc::now(); sqlx::query( "INSERT INTO user_profile (user_id, created_at, updated_at) VALUES ($1, $2, $2) ON CONFLICT (user_id) DO NOTHING", ) .bind(user_uid) .bind(now) .execute(self.ctx.db.writer()) .await .map_err(AppError::Database)?; self.find_user_profile(user_uid) .await? .ok_or(AppError::UserNotFound) } async fn find_user_profile( &self, user_uid: uuid::Uuid, ) -> Result, AppError> { sqlx::query_as::<_, UserProfile>( "SELECT user_id, full_name, company, location, website_url, twitter_username, \ timezone, language, profile_readme, created_at, updated_at \ FROM user_profile WHERE user_id = $1", ) .bind(user_uid) .fetch_optional(self.ctx.db.reader()) .await .map_err(AppError::Database) } }