Files
appks/service/user/profile.rs
T
zhenyi 420dedbc1e feat(service): expand service layer with new domain operations
- Add IM service modules: audit, channel roles, custom emojis, forum
  tags, integrations, invitations, repo links, slash commands, stages,
  voice, webhooks
- Add PR service modules: review requests, templates
- Add repo service modules: contributors, release assets, git extras
  (archive, branch rename, commit extras, diff/merge, tag, tree)
- Add user service: social (follow/block)
- Add internal auth service
- Update existing service modules with expanded functionality
- Remove deleted IM modules: articles, delivery trace, drafts,
  follows, messages, polls, presence, reactions, threads
2026-06-10 18:49:32 +08:00

99 lines
3.8 KiB
Rust

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<String>,
pub company: Option<String>,
pub location: Option<String>,
pub website_url: Option<String>,
pub twitter_username: Option<String>,
pub timezone: Option<String>,
pub language: Option<String>,
pub profile_readme: Option<String>,
}
impl UserService {
pub async fn user_profile(&self, ctx: &Session) -> Result<UserProfile, AppError> {
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<UserProfile, AppError> {
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<UserProfile, AppError> {
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)?;
// Read from writer to avoid replication lag
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.writer())
.await
.map_err(AppError::Database)?
.ok_or(AppError::UserNotFound)
}
async fn find_user_profile(
&self,
user_uid: uuid::Uuid,
) -> Result<Option<UserProfile>, 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)
}
}