fix: repair api server merge fallout
This commit is contained in:
@@ -2739,13 +2739,19 @@ mod tests {
|
||||
fn llm_response(content: &str) -> String {
|
||||
json!({
|
||||
"id": "resp_01",
|
||||
"choices": [
|
||||
"model": "test-model",
|
||||
"output": [
|
||||
{
|
||||
"message": {
|
||||
"content": content,
|
||||
}
|
||||
"type": "message",
|
||||
"content": [
|
||||
{
|
||||
"type": "output_text",
|
||||
"text": content,
|
||||
}
|
||||
],
|
||||
}
|
||||
]
|
||||
],
|
||||
"status": "completed"
|
||||
})
|
||||
.to_string()
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ use axum::{
|
||||
sse::{Event, Sse},
|
||||
},
|
||||
};
|
||||
use platform_llm::{LlmError, LlmMessage, LlmMessageRole, LlmTextProtocol, LlmTextRequest};
|
||||
use platform_llm::{LlmMessage, LlmMessageRole, LlmTextProtocol, LlmTextRequest};
|
||||
use serde_json::{Value, json};
|
||||
use shared_contracts::llm::{
|
||||
LlmChatCompletionRequest, LlmChatCompletionResponse, LlmChatMessagePayload, LlmChatMessageRole,
|
||||
|
||||
@@ -1276,6 +1276,58 @@ pub async fn swap_puzzle_pieces(
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn drag_puzzle_piece_or_group(
|
||||
State(state): State<AppState>,
|
||||
AxumPath(run_id): AxumPath<String>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||||
payload: Result<Json<DragPuzzlePieceRequest>, JsonRejection>,
|
||||
) -> Result<Json<Value>, Response> {
|
||||
let Json(payload) = payload.map_err(|error| {
|
||||
puzzle_error_response(
|
||||
&request_context,
|
||||
PUZZLE_RUNTIME_PROVIDER,
|
||||
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
|
||||
"provider": PUZZLE_RUNTIME_PROVIDER,
|
||||
"message": error.body_text(),
|
||||
})),
|
||||
)
|
||||
})?;
|
||||
ensure_non_empty(&request_context, PUZZLE_RUNTIME_PROVIDER, &run_id, "runId")?;
|
||||
ensure_non_empty(
|
||||
&request_context,
|
||||
PUZZLE_RUNTIME_PROVIDER,
|
||||
&payload.piece_id,
|
||||
"pieceId",
|
||||
)?;
|
||||
|
||||
let run = state
|
||||
.spacetime_client()
|
||||
.drag_puzzle_piece_or_group(PuzzleRunDragRecordInput {
|
||||
run_id,
|
||||
owner_user_id: authenticated.claims().user_id().to_string(),
|
||||
piece_id: payload.piece_id,
|
||||
target_row: payload.target_row,
|
||||
target_col: payload.target_col,
|
||||
dragged_at_micros: current_utc_micros(),
|
||||
})
|
||||
.await
|
||||
.map_err(|error| {
|
||||
puzzle_error_response(
|
||||
&request_context,
|
||||
PUZZLE_RUNTIME_PROVIDER,
|
||||
map_puzzle_client_error(error),
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(json_success_body(
|
||||
Some(&request_context),
|
||||
PuzzleRunResponse {
|
||||
run: map_puzzle_run_response(enrich_puzzle_run_author_name(&state, run).await),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn advance_puzzle_next_level(
|
||||
State(state): State<AppState>,
|
||||
AxumPath(run_id): AxumPath<String>,
|
||||
|
||||
@@ -10,7 +10,7 @@ use axum::{
|
||||
use platform_llm::{LlmMessage, LlmTextRequest};
|
||||
use serde::Deserialize;
|
||||
use serde_json::{Value, json};
|
||||
use shared_contracts::runtime_story::RuntimeStorySnapshotPayload;
|
||||
use shared_contracts::story::StoryRuntimeSnapshotPayload as RuntimeStorySnapshotPayload;
|
||||
use std::convert::Infallible;
|
||||
|
||||
use module_runtime_story::{
|
||||
|
||||
@@ -10,7 +10,7 @@ use axum::{
|
||||
use platform_llm::{LlmMessage, LlmTextRequest};
|
||||
use serde::Deserialize;
|
||||
use serde_json::{Value, json};
|
||||
use shared_contracts::runtime_story::RuntimeStorySnapshotPayload;
|
||||
use shared_contracts::story::StoryRuntimeSnapshotPayload as RuntimeStorySnapshotPayload;
|
||||
use std::convert::Infallible;
|
||||
|
||||
use crate::{
|
||||
|
||||
@@ -29,6 +29,11 @@ pub struct ChangePasswordResult {
|
||||
pub user: AuthUser,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct UpdateProfileResult {
|
||||
pub user: AuthUser,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct ResetPasswordResult {
|
||||
pub user: AuthUser,
|
||||
@@ -76,6 +81,7 @@ pub struct ConsumeWechatAuthStateResult {
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct BindWechatPhoneResult {
|
||||
pub user: AuthUser,
|
||||
pub activated_new_user: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
|
||||
@@ -29,6 +29,13 @@ pub struct ResetPasswordInput {
|
||||
pub new_password: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct UpdateProfileInput {
|
||||
pub user_id: String,
|
||||
pub display_name: Option<String>,
|
||||
pub avatar_url: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct SendPhoneCodeInput {
|
||||
pub phone_number: String,
|
||||
|
||||
@@ -55,11 +55,15 @@ pub struct AuthUser {
|
||||
pub public_user_code: String,
|
||||
pub username: String,
|
||||
pub display_name: String,
|
||||
#[serde(default)]
|
||||
pub avatar_url: Option<String>,
|
||||
pub phone_number_masked: Option<String>,
|
||||
pub login_method: AuthLoginMethod,
|
||||
pub binding_status: AuthBindingStatus,
|
||||
pub wechat_bound: bool,
|
||||
pub token_version: u64,
|
||||
#[serde(default)]
|
||||
pub created_at: String,
|
||||
}
|
||||
|
||||
/// 规范化后的手机号快照。
|
||||
|
||||
@@ -8,6 +8,9 @@ use std::{error::Error, fmt};
|
||||
pub enum PasswordEntryError {
|
||||
InvalidPhoneNumber,
|
||||
InvalidPasswordLength,
|
||||
InvalidDisplayName,
|
||||
InvalidAvatarDataUrl,
|
||||
EmptyProfileUpdate,
|
||||
InvalidPublicUserCode,
|
||||
InvalidCredentials,
|
||||
UserNotFound,
|
||||
@@ -61,6 +64,9 @@ impl fmt::Display for PasswordEntryError {
|
||||
match self {
|
||||
Self::InvalidPhoneNumber => f.write_str("手机号格式不正确"),
|
||||
Self::InvalidPasswordLength => f.write_str("密码长度需要在 6 到 128 位之间"),
|
||||
Self::InvalidDisplayName => f.write_str("昵称格式不正确"),
|
||||
Self::InvalidAvatarDataUrl => f.write_str("头像图片格式不正确"),
|
||||
Self::EmptyProfileUpdate => f.write_str("请至少修改昵称或头像"),
|
||||
Self::InvalidPublicUserCode => f.write_str("叙世号格式不正确"),
|
||||
Self::InvalidCredentials => f.write_str("手机号或密码错误"),
|
||||
Self::UserNotFound => f.write_str("用户不存在"),
|
||||
@@ -135,6 +141,9 @@ pub(crate) fn map_password_store_error(error: PasswordEntryError) -> RefreshSess
|
||||
PasswordEntryError::Store(message) => RefreshSessionError::Store(message),
|
||||
PasswordEntryError::InvalidPhoneNumber
|
||||
| PasswordEntryError::InvalidPasswordLength
|
||||
| PasswordEntryError::InvalidDisplayName
|
||||
| PasswordEntryError::InvalidAvatarDataUrl
|
||||
| PasswordEntryError::EmptyProfileUpdate
|
||||
| PasswordEntryError::InvalidPublicUserCode
|
||||
| PasswordEntryError::InvalidCredentials
|
||||
| PasswordEntryError::UserNotFound
|
||||
@@ -150,6 +159,9 @@ pub(crate) fn map_password_error_to_phone_error(error: PasswordEntryError) -> Ph
|
||||
PasswordEntryError::PasswordHash(message) => PhoneAuthError::PasswordHash(message),
|
||||
PasswordEntryError::InvalidPhoneNumber
|
||||
| PasswordEntryError::InvalidPasswordLength
|
||||
| PasswordEntryError::InvalidDisplayName
|
||||
| PasswordEntryError::InvalidAvatarDataUrl
|
||||
| PasswordEntryError::EmptyProfileUpdate
|
||||
| PasswordEntryError::InvalidPublicUserCode
|
||||
| PasswordEntryError::InvalidCredentials
|
||||
| PasswordEntryError::UserNotFound => PhoneAuthError::Store("用户仓储读取失败".to_string()),
|
||||
@@ -161,6 +173,9 @@ pub(crate) fn map_password_error_to_logout_error(error: PasswordEntryError) -> L
|
||||
PasswordEntryError::Store(message) => LogoutError::Store(message),
|
||||
PasswordEntryError::InvalidPhoneNumber
|
||||
| PasswordEntryError::InvalidPasswordLength
|
||||
| PasswordEntryError::InvalidDisplayName
|
||||
| PasswordEntryError::InvalidAvatarDataUrl
|
||||
| PasswordEntryError::EmptyProfileUpdate
|
||||
| PasswordEntryError::InvalidPublicUserCode
|
||||
| PasswordEntryError::InvalidCredentials
|
||||
| PasswordEntryError::UserNotFound
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
mod application;
|
||||
mod application;
|
||||
mod commands;
|
||||
mod domain;
|
||||
mod errors;
|
||||
@@ -203,6 +203,30 @@ impl PasswordEntryService {
|
||||
.map(|maybe_user| maybe_user.map(|stored| PublicUserSearchResult { user: stored.user }))
|
||||
}
|
||||
|
||||
pub fn update_profile(
|
||||
&self,
|
||||
input: UpdateProfileInput,
|
||||
) -> Result<UpdateProfileResult, PasswordEntryError> {
|
||||
let display_name = match input.display_name {
|
||||
Some(value) => Some(normalize_profile_display_name(value.as_str())?),
|
||||
None => None,
|
||||
};
|
||||
let avatar_url = match input.avatar_url {
|
||||
Some(value) => Some(normalize_profile_avatar_url(value.as_str())?),
|
||||
None => None,
|
||||
};
|
||||
if display_name.is_none() && avatar_url.is_none() {
|
||||
return Err(PasswordEntryError::EmptyProfileUpdate);
|
||||
}
|
||||
|
||||
let user = self
|
||||
.store
|
||||
.update_user_profile(&input.user_id, display_name, avatar_url)?
|
||||
.ok_or(PasswordEntryError::UserNotFound)?;
|
||||
|
||||
Ok(UpdateProfileResult { user })
|
||||
}
|
||||
|
||||
pub async fn change_password(
|
||||
&self,
|
||||
input: ChangePasswordInput,
|
||||
@@ -594,11 +618,14 @@ impl PhoneAuthService {
|
||||
return Err(PhoneAuthError::UserStateMismatch);
|
||||
}
|
||||
|
||||
let merged_user = self
|
||||
let (merged_user, activated_new_user) = self
|
||||
.store
|
||||
.bind_wechat_phone_to_user(&input.user_id, normalized_phone)?;
|
||||
|
||||
Ok(BindWechatPhoneResult { user: merged_user })
|
||||
Ok(BindWechatPhoneResult {
|
||||
user: merged_user,
|
||||
activated_new_user,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -985,6 +1012,36 @@ impl InMemoryAuthStore {
|
||||
.cloned())
|
||||
}
|
||||
|
||||
fn update_user_profile(
|
||||
&self,
|
||||
user_id: &str,
|
||||
display_name: Option<String>,
|
||||
avatar_url: Option<String>,
|
||||
) -> Result<Option<AuthUser>, PasswordEntryError> {
|
||||
let mut state = self
|
||||
.inner
|
||||
.lock()
|
||||
.map_err(|_| PasswordEntryError::Store("用户仓储锁已中毒".to_string()))?;
|
||||
|
||||
for stored_user in state.users_by_username.values_mut() {
|
||||
if stored_user.user.id != user_id {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(display_name) = display_name {
|
||||
stored_user.user.display_name = display_name;
|
||||
}
|
||||
if let Some(avatar_url) = avatar_url {
|
||||
stored_user.user.avatar_url = Some(avatar_url);
|
||||
}
|
||||
let next_user = stored_user.user.clone();
|
||||
self.persist_password_state(&state)?;
|
||||
return Ok(Some(next_user));
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn create_phone_user(
|
||||
&self,
|
||||
phone_number: PhoneNumberSnapshot,
|
||||
@@ -1001,6 +1058,9 @@ impl InMemoryAuthStore {
|
||||
));
|
||||
}
|
||||
|
||||
let created_at = format_rfc3339(OffsetDateTime::now_utc()).map_err(|message| {
|
||||
PhoneAuthError::Store(format!("用户创建时间格式化失败:{message}"))
|
||||
})?;
|
||||
let sequence = state.next_user_id;
|
||||
let user_id = format!("user_{sequence:08}");
|
||||
let public_user_code = build_public_user_code(sequence);
|
||||
@@ -1011,11 +1071,13 @@ impl InMemoryAuthStore {
|
||||
public_user_code,
|
||||
username: username.clone(),
|
||||
display_name,
|
||||
avatar_url: None,
|
||||
phone_number_masked: Some(phone_number.masked_national_number.clone()),
|
||||
login_method: AuthLoginMethod::Phone,
|
||||
binding_status: AuthBindingStatus::Active,
|
||||
wechat_bound: false,
|
||||
token_version: 1,
|
||||
created_at,
|
||||
};
|
||||
state
|
||||
.phone_to_user_id
|
||||
@@ -1048,6 +1110,9 @@ impl InMemoryAuthStore {
|
||||
return Err(PasswordEntryError::InvalidCredentials);
|
||||
}
|
||||
|
||||
let created_at = format_rfc3339(OffsetDateTime::now_utc()).map_err(|message| {
|
||||
PasswordEntryError::Store(format!("用户创建时间格式化失败:{message}"))
|
||||
})?;
|
||||
let sequence = state.next_user_id;
|
||||
let user_id = format!("user_{sequence:08}");
|
||||
let public_user_code = build_public_user_code(sequence);
|
||||
@@ -1058,11 +1123,13 @@ impl InMemoryAuthStore {
|
||||
public_user_code,
|
||||
username: username.clone(),
|
||||
display_name,
|
||||
avatar_url: None,
|
||||
phone_number_masked: Some(phone_number.masked_national_number.clone()),
|
||||
login_method: AuthLoginMethod::Password,
|
||||
binding_status: AuthBindingStatus::Active,
|
||||
wechat_bound: false,
|
||||
token_version: 1,
|
||||
created_at,
|
||||
};
|
||||
state
|
||||
.phone_to_user_id
|
||||
@@ -1091,11 +1158,15 @@ impl InMemoryAuthStore {
|
||||
.lock()
|
||||
.map_err(|_| WechatAuthError::Store("用户仓储锁已中毒".to_string()))?;
|
||||
|
||||
let created_at = format_rfc3339(OffsetDateTime::now_utc()).map_err(|message| {
|
||||
WechatAuthError::Store(format!("用户创建时间格式化失败:{message}"))
|
||||
})?;
|
||||
let sequence = state.next_user_id;
|
||||
let user_id = format!("user_{sequence:08}");
|
||||
let public_user_code = build_public_user_code(sequence);
|
||||
state.next_user_id += 1;
|
||||
let username = build_system_username("wechat", state.next_user_id);
|
||||
let avatar_url = normalize_optional_string(profile.avatar_url.clone());
|
||||
let display_name = profile
|
||||
.display_name
|
||||
.as_deref()
|
||||
@@ -1108,11 +1179,13 @@ impl InMemoryAuthStore {
|
||||
public_user_code,
|
||||
username: username.clone(),
|
||||
display_name,
|
||||
avatar_url: avatar_url.clone(),
|
||||
phone_number_masked: None,
|
||||
login_method: AuthLoginMethod::Wechat,
|
||||
binding_status: AuthBindingStatus::PendingBindPhone,
|
||||
wechat_bound: true,
|
||||
token_version: 1,
|
||||
created_at,
|
||||
};
|
||||
state.users_by_username.insert(
|
||||
username,
|
||||
@@ -1128,7 +1201,7 @@ impl InMemoryAuthStore {
|
||||
provider_uid: normalize_required_string(&profile.provider_uid).unwrap_or_default(),
|
||||
provider_union_id: normalize_optional_string(profile.provider_union_id),
|
||||
display_name: normalize_optional_string(profile.display_name),
|
||||
avatar_url: normalize_optional_string(profile.avatar_url),
|
||||
avatar_url,
|
||||
};
|
||||
if let Some(provider_union_id) = identity.provider_union_id.clone() {
|
||||
state
|
||||
@@ -1454,7 +1527,7 @@ impl InMemoryAuthStore {
|
||||
&self,
|
||||
pending_user_id: &str,
|
||||
phone_number: PhoneNumberSnapshot,
|
||||
) -> Result<AuthUser, PhoneAuthError> {
|
||||
) -> Result<(AuthUser, bool), PhoneAuthError> {
|
||||
let mut state = self
|
||||
.inner
|
||||
.lock()
|
||||
@@ -1501,7 +1574,7 @@ impl InMemoryAuthStore {
|
||||
let next_user = target_user.user.clone();
|
||||
self.persist_phone_state(&state)?;
|
||||
|
||||
return Ok(next_user);
|
||||
return Ok((next_user, false));
|
||||
}
|
||||
|
||||
state
|
||||
@@ -1520,7 +1593,7 @@ impl InMemoryAuthStore {
|
||||
let next_user = stored_user.user.clone();
|
||||
self.persist_phone_state(&state)?;
|
||||
|
||||
Ok(next_user)
|
||||
Ok((next_user, true))
|
||||
}
|
||||
|
||||
fn find_session_by_refresh_token_hash(
|
||||
@@ -1819,6 +1892,40 @@ async fn verify_stored_password_user(
|
||||
})
|
||||
}
|
||||
|
||||
fn normalize_profile_display_name(value: &str) -> Result<String, PasswordEntryError> {
|
||||
let Some(display_name) = normalize_required_string(value) else {
|
||||
return Err(PasswordEntryError::InvalidDisplayName);
|
||||
};
|
||||
let length = display_name.chars().count();
|
||||
if !(2..=20).contains(&length) {
|
||||
return Err(PasswordEntryError::InvalidDisplayName);
|
||||
}
|
||||
if !display_name.chars().all(|character| {
|
||||
character == '_'
|
||||
|| character.is_ascii_alphanumeric()
|
||||
|| is_common_chinese_character(character)
|
||||
}) {
|
||||
return Err(PasswordEntryError::InvalidDisplayName);
|
||||
}
|
||||
|
||||
Ok(display_name)
|
||||
}
|
||||
|
||||
fn normalize_profile_avatar_url(value: &str) -> Result<String, PasswordEntryError> {
|
||||
let Some(avatar_url) = normalize_required_string(value) else {
|
||||
return Err(PasswordEntryError::InvalidAvatarDataUrl);
|
||||
};
|
||||
if !avatar_url.starts_with("data:image/") || !avatar_url.contains(";base64,") {
|
||||
return Err(PasswordEntryError::InvalidAvatarDataUrl);
|
||||
}
|
||||
|
||||
Ok(avatar_url)
|
||||
}
|
||||
|
||||
fn is_common_chinese_character(character: char) -> bool {
|
||||
('\u{4e00}'..='\u{9fff}').contains(&character)
|
||||
}
|
||||
|
||||
fn build_random_password_seed() -> String {
|
||||
format!(
|
||||
"seed_{}_{}",
|
||||
|
||||
Reference in New Issue
Block a user