use axum::{ Json, extract::{Extension, State}, http::StatusCode, }; use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64_STANDARD}; use image::GenericImageView; use module_auth::{PasswordEntryError, UpdateProfileInput}; use shared_contracts::auth::{ProfileUpdateRequest, ProfileUpdateResponse}; use crate::{ api_response::json_success_body, auth::AuthenticatedAccessToken, auth_payload::map_auth_user_payload, http_error::AppError, request_context::RequestContext, state::AppState, }; const MAX_AVATAR_BYTES: usize = 5 * 1024 * 1024; const AVATAR_SIZE_PX: u32 = 256; pub async fn update_profile_identity( State(state): State, Extension(request_context): Extension, Extension(authenticated): Extension, Json(payload): Json, ) -> Result, AppError> { if let Some(avatar_data_url) = payload.avatar_data_url.as_deref() { validate_avatar_data_url(avatar_data_url)?; } let result = state .password_entry_service() .update_profile(UpdateProfileInput { user_id: authenticated.claims().user_id().to_string(), display_name: payload.display_name, avatar_url: payload.avatar_data_url, }) .map_err(map_profile_update_error)?; state .sync_auth_store_snapshot_to_spacetime() .await .map_err(|error| { AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_message(error.to_string()) })?; Ok(json_success_body( Some(&request_context), ProfileUpdateResponse { user: map_auth_user_payload(result.user), }, )) } fn validate_avatar_data_url(value: &str) -> Result<(), AppError> { let Some((header, payload)) = value.trim().split_once(',') else { return Err(invalid_avatar_error("头像图片格式不正确")); }; if !matches!( header, "data:image/png;base64" | "data:image/jpeg;base64" | "data:image/webp;base64" ) { return Err(invalid_avatar_error("头像仅支持 jpg、png、webp")); } let bytes = BASE64_STANDARD .decode(payload) .map_err(|_| invalid_avatar_error("头像图片格式不正确"))?; if bytes.len() > MAX_AVATAR_BYTES { return Err(invalid_avatar_error("头像图片不能超过 5MB")); } let image = image::load_from_memory(&bytes).map_err(|_| invalid_avatar_error("头像图片格式不正确"))?; let (width, height) = image.dimensions(); if width != AVATAR_SIZE_PX || height != AVATAR_SIZE_PX { return Err(invalid_avatar_error("头像裁剪尺寸需要为 256x256")); } Ok(()) } fn invalid_avatar_error(message: &'static str) -> AppError { AppError::from_status(StatusCode::BAD_REQUEST).with_message(message) } fn map_profile_update_error(error: PasswordEntryError) -> AppError { match error { PasswordEntryError::InvalidDisplayName | PasswordEntryError::InvalidAvatarDataUrl | PasswordEntryError::EmptyProfileUpdate => { AppError::from_status(StatusCode::BAD_REQUEST).with_message(error.to_string()) } PasswordEntryError::UserNotFound => AppError::from_status(StatusCode::UNAUTHORIZED) .with_message("当前登录态已失效,请重新登录"), PasswordEntryError::Store(_) | PasswordEntryError::PasswordHash(_) => { AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_message(error.to_string()) } PasswordEntryError::InvalidPhoneNumber | PasswordEntryError::InvalidPasswordLength | PasswordEntryError::InvalidPublicUserCode | PasswordEntryError::InvalidCredentials => { AppError::from_status(StatusCode::BAD_REQUEST).with_message(error.to_string()) } } }