alibi_api/plugins/user_management/
update.rs1use alibi_core::entity::AuthUser;
2use alibi_core::utils::username::UsernameConfig;
3use alibi_core::{
4 AuthContext, AuthError, AuthRequest, AuthResponse, AuthResult, ErrorCodeMessageResponse,
5 UpdateUser, UpdateUserRequest,
6};
7
8#[expect(
10 clippy::too_many_lines,
11 reason = "Keep field validation and user-update callbacks adjacent to the persistence operation"
12)]
13pub async fn handle_update_user<S: alibi_core::AuthSchema>(
14 req: &AuthRequest,
15 context: &AuthContext<S>,
16) -> AuthResult<AuthResponse> {
17 let (current_user, current_session) =
18 context.require_cached_session(req).await.map_err(|error| {
19 if matches!(
20 error,
21 AuthError::Unauthenticated | AuthError::SessionNotFound | AuthError::UserNotFound
22 ) {
23 AuthError::Upstream {
24 status: 401,
25 code: "UNAUTHORIZED",
26 message: "Unauthorized",
27 }
28 } else {
29 error
30 }
31 })?;
32 let body: serde_json::Value = req
33 .body_as_json()
34 .map_err(|e| AuthError::bad_request(format!("Invalid JSON: {e}")))?;
35 let Some(body) = body.as_object() else {
36 let actual = match &body {
37 serde_json::Value::Null => "null",
38 serde_json::Value::Bool(_) => "boolean",
39 serde_json::Value::Number(_) => "number",
40 serde_json::Value::String(_) => "string",
41 serde_json::Value::Array(_) => "array",
42 serde_json::Value::Object(_) => "record",
43 };
44 return Ok(AuthResponse::json(
45 400,
46 &ErrorCodeMessageResponse {
47 code: Some("VALIDATION_ERROR".to_owned()),
48 message: format!("[body] Invalid input: expected record, received {actual}"),
49 },
50 )?);
51 };
52
53 if body.contains_key("email") {
54 return Err(AuthError::bad_request("Email can not be updated"));
55 }
56
57 if body.contains_key("role") {
60 return Err(AuthError::Api {
61 status: 400,
62 code: Some("FIELD_NOT_ALLOWED".into()),
63 message: "role is not allowed to be set".into(),
64 });
65 }
66
67 let raw_body: alibi_core::utils::json::JsValue = req.body_as_json()?;
68 crate::plugins::last_login_method::reject_last_login_method_input(
69 context,
70 raw_body.get("lastLoginMethod"),
71 )?;
72
73 let update_req: UpdateUserRequest =
74 serde_json::from_value(serde_json::Value::Object(body.clone()))
75 .map_err(|e| AuthError::bad_request(format!("Invalid JSON: {e}")))?;
76 let policy = context.extensions.get::<UsernameConfig>();
77 if let Some(policy) = &policy {
78 if let Some(value) = &update_req.username {
79 policy.validate_hook_value(value).await?;
80 let normalized = policy.normalize(value)?;
81 let current_username = current_user.username();
82 if policy.immutable_username
83 && current_username
84 .as_ref()
85 .is_some_and(|value| !value.is_empty() && *value != normalized)
86 {
87 return Err(AuthError::Upstream {
88 status: 400,
89 code: "USERNAME_IS_IMMUTABLE",
90 message: "Username cannot be updated",
91 });
92 }
93 if let Some(existing_user) = context.database.get_user_by_username(&normalized).await?
94 && existing_user.id() != current_user.id()
95 {
96 return Err(AuthError::Upstream {
97 status: 400,
98 code: "USERNAME_IS_ALREADY_TAKEN",
99 message: "Username is already taken. Please try another.",
100 });
101 }
102 }
103 if let Some(value) = &update_req.display_username {
104 policy.validate_display(value).await?;
105 }
106 }
107 let mut input_fields = raw_body
108 .as_object()
109 .ok_or_else(|| AuthError::bad_request("Invalid JSON object"))?
110 .clone();
111 if policy.is_none() {
112 drop(input_fields.shift_remove("username"));
113 }
114 if policy
115 .as_ref()
116 .is_none_or(|policy| !policy.include_display_username)
117 {
118 drop(input_fields.shift_remove("displayUsername"));
119 }
120 let additional_fields = context
121 .parse_user_fields(&input_fields, false)
122 .map_err(|error| match error {
123 alibi_core::field_policy::FieldInputError::Validation { code, message } => {
124 AuthError::Api {
125 status: 400,
126 code: Some(code.into()),
127 message,
128 }
129 }
130 alibi_core::field_policy::FieldInputError::Transform(error) => error,
131 })?;
132 let username = additional_fields
133 .get("username")
134 .and_then(alibi_core::utils::json::JsValue::as_str)
135 .map(str::to_owned);
136 let display_username = additional_fields
137 .get("displayUsername")
138 .and_then(alibi_core::utils::json::JsValue::as_str)
139 .map(str::to_owned);
140
141 let clear_phone = context
142 .get_metadata("phone-number.enabled")
143 .and_then(serde_json::Value::as_bool)
144 == Some(true)
145 && body.get("phoneNumber") == Some(&serde_json::Value::Null);
146 let has_changes = additional_fields.has_input_fields()
147 || clear_phone
148 || update_req.name.is_some()
149 || update_req.image.is_some()
150 || username.is_some()
151 || display_username.is_some()
152 || update_req.metadata.is_some();
153 if !has_changes {
154 return Err(AuthError::bad_request("No fields to update"));
155 }
156
157 let update_user = UpdateUser {
158 provider_email_verified: None,
159 provider_name: None,
160 provider_image: None,
161 additional_fields,
162 is_anonymous: None,
163 phone_number: clear_phone.then_some(None),
164 phone_number_verified: None,
165 last_login_method: None,
166 email: None,
167 name: update_req.name,
168 image: update_req.image,
169 email_verified: None,
170 username,
171 display_username,
172 role: None,
173 banned: None,
174 ban_reason: None,
175 ban_expires: None,
176 two_factor_enabled: None,
177 metadata: update_req.metadata,
178 };
179
180 let publication = match context
181 .database
182 .update_user_record(¤t_user.id(), update_user.clone())
183 .await
184 {
185 Ok(updated_user) => alibi_core::CacheVersionContext::created(
186 updated_user.clone(),
187 current_session.clone(),
188 context.user_view(&updated_user),
189 current_session.clone(),
190 ),
191 Err(AuthError::UserNotFound) => {
192 let mut user = context.user_view(¤t_user);
195 if let Some(name) = update_user.name {
196 user.name = Some(name);
197 }
198 if let Some(image) = update_user.image {
199 user.image = Some(image);
200 }
201 if let Some(username) = update_user.username {
202 user.username = Some(username);
203 }
204 if let Some(display_username) = update_user.display_username {
205 user.display_username = Some(display_username);
206 }
207 if let Some(metadata) = update_user.metadata {
208 user.metadata = metadata;
209 }
210 for (name, value) in update_user.additional_fields {
211 drop(user.extension_fields.insert(name, value.to_json_value()?));
212 }
213 if let Some(phone_number) = update_user.phone_number {
214 user.phone_number = phone_number;
215 drop(
216 user.extension_fields
217 .insert("phoneNumber".into(), serde_json::Value::Null),
218 );
219 }
220 alibi_core::CacheVersionContext::created(
221 user.clone(),
222 current_session.clone(),
223 user,
224 current_session.clone(),
225 )
226 }
227 Err(error) => return Err(error),
228 };
229 alibi_core::session::cookie_cache::runtime::emit_issuance_snapshot(context, publication)
230 .await?;
231
232 let mut response = AuthResponse::json(200, &alibi_core::StatusResponse { status: true })?;
233
234 if let Some(token) = context.session_manager().extract_session_token(req) {
235 let cookie_header =
236 alibi_core::utils::cookie_utils::create_session_cookie(&token, &context.config)?;
237 response = response.with_header("Set-Cookie", cookie_header);
238 }
239
240 Ok(response)
241}