parse_rs/user.rs
1// src/user.rs
2
3use crate::object::{deserialize_string_to_option_parse_date, deserialize_string_to_parse_date};
4use crate::types::ParseDate;
5use crate::ParseError;
6use reqwest::Method;
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
9
10/// Represents a Parse Server User object.
11///
12/// This struct contains standard fields for a user, such as `objectId`, `username`, `email`,
13/// `emailVerified`, `sessionToken`, `createdAt`, and `updatedAt`.
14/// It is used to deserialize user data received from the Parse Server and can also be
15/// (though less commonly for `ParseUser` itself) used for creating or updating user objects.
16#[derive(Debug, Serialize, Deserialize, Clone)]
17pub struct ParseUser {
18 #[serde(rename = "objectId")]
19 pub object_id: Option<String>,
20 pub username: String,
21 pub email: Option<String>,
22 #[serde(rename = "emailVerified")]
23 pub email_verified: Option<bool>,
24 #[serde(rename = "sessionToken")]
25 pub session_token: Option<String>,
26 #[serde(
27 rename = "createdAt",
28 deserialize_with = "deserialize_string_to_option_parse_date",
29 skip_serializing_if = "Option::is_none"
30 )]
31 pub created_at: Option<ParseDate>,
32 #[serde(
33 rename = "updatedAt",
34 deserialize_with = "deserialize_string_to_option_parse_date",
35 skip_serializing_if = "Option::is_none"
36 )]
37 pub updated_at: Option<ParseDate>,
38}
39
40// New struct for signup response
41/// Represents the successful response from a user signup operation.
42///
43/// It includes the `objectId` of the newly created user, their `sessionToken`,
44/// and the `createdAt` timestamp.
45#[derive(Debug, Deserialize, Clone)]
46pub struct SignupResponse {
47 #[serde(rename = "objectId")]
48 pub object_id: String,
49 #[serde(rename = "sessionToken")]
50 pub session_token: String,
51 #[serde(
52 rename = "createdAt",
53 deserialize_with = "deserialize_string_to_parse_date"
54 )]
55 pub created_at: ParseDate,
56}
57
58// Request body for user signup
59/// Represents the data required to sign up a new user.
60///
61/// This struct is typically serialized and sent as the body of a signup request.
62/// It includes `username`, `password`, and an optional `email`.
63/// Additional fields can be included by using a more generic type like `ParseObject` or `HashMap<String, Value>`
64/// with the `signup` method if the server is configured to accept them.
65#[derive(Serialize, Debug)]
66pub struct SignupRequest<'a> {
67 pub username: &'a str,
68 pub password: &'a str,
69 #[serde(skip_serializing_if = "Option::is_none")]
70 pub email: Option<&'a str>,
71}
72
73// Request body for user login
74/// Represents the data required to log in an existing user.
75///
76/// This struct is typically serialized and sent as the body of a login request.
77/// It includes `username` and `password`.
78#[derive(Serialize, Debug)]
79pub struct LoginRequest<'a> {
80 pub username: &'a str,
81 pub password: &'a str,
82}
83
84// Request body for password reset request
85/// Represents the data required to request a password reset for a user.
86///
87/// This struct is typically serialized and sent as the body of a password reset request.
88/// It includes the `email` address of the user requesting the reset.
89#[derive(Serialize, Debug)]
90pub struct PasswordResetRequest<'a> {
91 pub email: &'a str,
92}
93
94/// Provides methods for managing user authentication and user-specific operations.
95///
96/// An instance of `ParseUserHandle` is obtained by calling the [`user()`](crate::Parse::user)
97/// method on a `Parse` instance. It allows for operations such as signing up new users,
98/// logging in existing users, fetching the current user's details, logging out, and requesting password resets.
99///
100/// The handle maintains a mutable reference to the `Parse` to update its session state (e.g., `session_token`)
101/// upon successful login or signup, and to clear it on logout.
102pub struct ParseUserHandle<'a> {
103 client: &'a mut crate::Parse,
104}
105
106impl ParseUserHandle<'_> {
107 /// Creates a new `ParseUserHandle`.
108 ///
109 /// This constructor is typically called by `Parse::user()`.
110 ///
111 /// # Arguments
112 ///
113 /// * `client`: A mutable reference to the `Parse` instance that this handle will operate upon.
114 pub fn new(client: &mut crate::Parse) -> ParseUserHandle<'_> {
115 ParseUserHandle { client }
116 }
117
118 // User management methods
119 /// Signs up a new user with the Parse Server.
120 ///
121 /// This method sends the provided user data to the `/users` endpoint. Upon successful signup,
122 /// the Parse Server returns the new user's `objectId`, a `sessionToken`, and `createdAt` timestamp.
123 /// The `sessionToken` is automatically stored in the `Parse` instance, making the new user
124 /// the current authenticated user for subsequent requests.
125 ///
126 /// # Type Parameters
127 ///
128 /// * `T`: The type of the `user_data` argument. This type must implement `Serialize`, `Send`, and `Sync`.
129 /// Commonly, this will be [`SignupRequest`](crate::user::SignupRequest) for standard username/password/email signups,
130 /// but can also be a `ParseObject` or a `HashMap<String, Value>` if you need to include additional
131 /// custom fields during signup (assuming your Parse Server is configured to allow this).
132 ///
133 /// # Arguments
134 ///
135 /// * `user_data`: A reference to the data for the new user. This typically includes `username` and `password`,
136 /// and can optionally include `email` and other custom fields.
137 ///
138 /// # Returns
139 ///
140 /// A `Result` containing a [`SignupResponse`](crate::user::SignupResponse) if the signup is successful,
141 /// or a `ParseError` if the signup fails (e.g., username taken, invalid data, network issue).
142 ///
143 /// # Examples
144 ///
145 /// ```rust,no_run
146 /// use parse_rs::{Parse, ParseError, user::SignupRequest};
147 /// use serde_json::Value;
148 /// use std::collections::HashMap;
149 ///
150 /// # #[tokio::main]
151 /// # async fn main() -> Result<(), ParseError> {
152 /// # let server_url = std::env::var("PARSE_SERVER_URL").unwrap_or_else(|_| "http://localhost:1338/parse".to_string());
153 /// # let app_id = std::env::var("PARSE_APP_ID").unwrap_or_else(|_| "myAppId".to_string());
154 /// # let mut client = Parse::new(&server_url, &app_id, None, None, None)?;
155 ///
156 /// // Example 1: Using SignupRequest for standard fields
157 /// let signup_details = SignupRequest {
158 /// username: "new_user_1",
159 /// password: "securePassword123",
160 /// email: Some("user1@example.com"),
161 /// };
162 ///
163 /// match client.user().signup(&signup_details).await {
164 /// Ok(response) => {
165 /// println!("User '{}' signed up successfully! ObjectId: {}, Session Token: {}",
166 /// signup_details.username, response.object_id, response.session_token);
167 /// assert_eq!(client.session_token(), Some(response.session_token.as_str()));
168 /// }
169 /// Err(e) => eprintln!("Signup failed for user '{}': {}", signup_details.username, e),
170 /// }
171 ///
172 /// // Example 2: Using HashMap for additional custom fields (if server allows)
173 /// let mut custom_signup_data = HashMap::new();
174 /// custom_signup_data.insert("username".to_string(), Value::String("new_user_2".to_string()));
175 /// custom_signup_data.insert("password".to_string(), Value::String("anotherSecurePass456".to_string()));
176 /// custom_signup_data.insert("email".to_string(), Value::String("user2@example.com".to_string()));
177 /// custom_signup_data.insert("customField".to_string(), Value::String("customValue".to_string()));
178 /// custom_signup_data.insert("age".to_string(), Value::Number(30.into()));
179 ///
180 /// // match client.user().signup(&custom_signup_data).await {
181 /// // Ok(response) => {
182 /// // println!("User with custom data signed up! ObjectId: {}, Session: {}",
183 /// // response.object_id, response.session_token);
184 /// // }
185 /// // Err(e) => eprintln!("Custom signup failed: {}", e),
186 /// // }
187 /// # Ok(())
188 /// # }
189 /// ```
190 pub async fn signup<T: Serialize + Send + Sync>(
191 &mut self,
192 user_data: &T, // Changed from SignupRequest to generic T for flexibility if ParseObject is used
193 ) -> Result<SignupResponse, ParseError> {
194 // Endpoint for signup is typically "users"
195 match self
196 .client
197 ._request::<_, SignupResponse>(Method::POST, "users", Some(user_data), false, None)
198 .await
199 {
200 Ok(response) => {
201 // Assuming SignupResponse contains a session_token field
202 self.client.session_token = Some(response.session_token.clone());
203 Ok(response)
204 }
205 Err(e) => Err(e),
206 }
207 }
208
209 /// Logs in an existing user with the Parse Server.
210 ///
211 /// This method sends the provided user credentials (typically username and password) to the `/login` endpoint.
212 /// Upon successful login, the Parse Server returns the full user object, including a `sessionToken`.
213 /// This `sessionToken` is automatically stored in the `Parse` instance, making the user
214 /// the current authenticated user for subsequent requests.
215 ///
216 /// # Type Parameters
217 ///
218 /// * `T`: The type of the `user_data` argument. This type must implement `Serialize`, `Send`, and `Sync`.
219 /// Commonly, this will be [`LoginRequest`](crate::user::LoginRequest) for standard username/password logins.
220 /// It could also be a `HashMap<String, Value>` if the server supports other login mechanisms via the same endpoint,
221 /// though this is less common for the standard `/login` route.
222 ///
223 /// # Arguments
224 ///
225 /// * `user_data`: A reference to the credentials for the user to log in. This typically includes `username` and `password`.
226 ///
227 /// # Returns
228 ///
229 /// A `Result` containing the full [`ParseUser`](crate::user::ParseUser) object if the login is successful,
230 /// or a `ParseError` if the login fails (e.g., invalid credentials, user not found, network issue).
231 ///
232 /// # Examples
233 ///
234 /// ```rust,no_run
235 /// use parse_rs::{Parse, ParseError, user::LoginRequest};
236 ///
237 /// # #[tokio::main]
238 /// # async fn main() -> Result<(), ParseError> {
239 /// # let server_url = std::env::var("PARSE_SERVER_URL").unwrap_or_else(|_| "http://localhost:1338/parse".to_string());
240 /// # let app_id = std::env::var("PARSE_APP_ID").unwrap_or_else(|_| "myAppId".to_string());
241 /// # let mut client = Parse::new(&server_url, &app_id, None, None, None)?;
242 ///
243 /// // Assume "test_user" was previously signed up
244 /// let login_details = LoginRequest {
245 /// username: "test_user",
246 /// password: "password123",
247 /// };
248 ///
249 /// match client.user().login(&login_details).await {
250 /// Ok(logged_in_user) => {
251 /// println!("User '{}' logged in successfully! Session Token: {}",
252 /// logged_in_user.username,
253 /// logged_in_user.session_token.as_deref().unwrap_or("N/A"));
254 /// assert_eq!(client.session_token(), logged_in_user.session_token.as_deref());
255 /// assert_eq!(logged_in_user.username, "test_user");
256 /// }
257 /// Err(e) => eprintln!("Login failed for user '{}': {}", login_details.username, e),
258 /// }
259 /// # Ok(())
260 /// # }
261 /// ```
262 pub async fn login<T: Serialize + Send + Sync>(
263 &mut self,
264 user_data: &T, // Changed from LoginRequest to generic T
265 ) -> Result<ParseUser, ParseError> {
266 // Endpoint for login is typically "login"
267 match self
268 .client
269 ._request::<_, ParseUser>(Method::POST, "login", Some(user_data), false, None)
270 .await
271 {
272 Ok(user_response) => {
273 self.client.session_token = user_response.session_token.clone();
274 Ok(user_response)
275 }
276 Err(e) => Err(e),
277 }
278 }
279
280 // GET /users/me - requires session token
281 /// Fetches the details of the currently authenticated user.
282 ///
283 /// This method makes a GET request to the `/users/me` endpoint, which requires a valid
284 /// session token to be present in the `Parse` (set automatically after a successful
285 /// login or signup).
286 ///
287 /// If no session token is available in the client, this method will return a
288 /// `ParseError::SessionTokenMissing` error without making a network request.
289 ///
290 /// # Returns
291 ///
292 /// A `Result` containing the [`ParseUser`](crate::user::ParseUser) object for the currently
293 /// authenticated user if successful, or a `ParseError` if the request fails (e.g., session token
294 /// is invalid or expired, network issue, or no session token is present).
295 ///
296 /// # Examples
297 ///
298 /// ```rust,no_run
299 /// use parse_rs::{Parse, ParseError, user::LoginRequest};
300 ///
301 /// # #[tokio::main]
302 /// # async fn main() -> Result<(), ParseError> {
303 /// # let server_url = std::env::var("PARSE_SERVER_URL").unwrap_or_else(|_| "http://localhost:1338/parse".to_string());
304 /// # let app_id = std::env::var("PARSE_APP_ID").unwrap_or_else(|_| "myAppId".to_string());
305 /// # let mut client = Parse::new(&server_url, &app_id, None, None, None)?;
306 ///
307 /// // First, log in a user (or sign them up)
308 /// // let login_details = LoginRequest { username: "test_user", password: "password123" };
309 /// // client.user().login(&login_details).await?;
310 ///
311 /// if client.is_authenticated() {
312 /// match client.user().me().await {
313 /// Ok(current_user) => {
314 /// println!("Current user: {}, Email: {:?}",
315 /// current_user.username, current_user.email.as_deref().unwrap_or("N/A"));
316 /// // The sessionToken field in the returned ParseUser object from /users/me
317 /// // might be the same or a new one depending on server configuration.
318 /// // The client's session token remains the one used for the request.
319 /// }
320 /// Err(e) => eprintln!("Failed to fetch current user: {}", e),
321 /// }
322 /// } else {
323 /// println!("No user is currently authenticated.");
324 /// }
325 /// # Ok(())
326 /// # }
327 /// ```
328 pub async fn me(&self) -> Result<ParseUser, ParseError> {
329 // Removed &mut self as it only reads session token
330 if self.client.session_token.is_none() {
331 return Err(ParseError::SessionTokenMissing);
332 }
333 // current_user does not take a body
334 self.client
335 ._request(Method::GET, "users/me", None::<&Value>, false, None)
336 .await
337 }
338
339 // POST /logout - requires session token
340 /// Logs out the currently authenticated user.
341 ///
342 /// This method sends a POST request to the `/logout` endpoint using the current session token
343 /// stored in the `Parse`. If successful, the Parse Server invalidates the session token.
344 /// This method also clears the `session_token` from the `Parse` instance, effectively
345 /// ending the current user's session on the client-side as well.
346 ///
347 /// If no session token is available in the client, this method will return a
348 /// `ParseError::SessionTokenMissing` error without making a network request.
349 ///
350 /// # Returns
351 ///
352 /// A `Result` containing `()` (an empty tuple) if the logout is successful, or a `ParseError`
353 /// if the request fails (e.g., session token already invalid, network issue, or no session token
354 /// is present).
355 ///
356 /// # Examples
357 ///
358 /// ```rust,no_run
359 /// use parse_rs::{Parse, ParseError, user::LoginRequest};
360 ///
361 /// # #[tokio::main]
362 /// # async fn main() -> Result<(), ParseError> {
363 /// # let server_url = std::env::var("PARSE_SERVER_URL").unwrap_or_else(|_| "http://localhost:1338/parse".to_string());
364 /// # let app_id = std::env::var("PARSE_APP_ID").unwrap_or_else(|_| "myAppId".to_string());
365 /// # let mut client = Parse::new(&server_url, &app_id, None, None, None)?;
366 ///
367 /// // First, ensure a user is logged in
368 /// // let login_details = LoginRequest { username: "test_user", password: "password123" };
369 /// // client.user().login(&login_details).await?;
370 ///
371 /// if client.is_authenticated() {
372 /// println!("User is authenticated with token: {:?}", client.session_token());
373 /// match client.user().logout().await {
374 /// Ok(_) => {
375 /// println!("User logged out successfully.");
376 /// assert!(!client.is_authenticated(), "Client should not be authenticated after logout.");
377 /// assert!(client.session_token().is_none(), "Session token should be cleared after logout.");
378 /// }
379 /// Err(e) => eprintln!("Logout failed: {}", e),
380 /// }
381 /// } else {
382 /// println!("No user was logged in to log out.");
383 /// }
384 ///
385 /// // Attempting logout again when not authenticated should fail (or do nothing gracefully)
386 /// // match client.user().logout().await {
387 /// // Err(ParseError::SessionTokenMissing) => println!("Correctly failed: Session token missing for logout."),
388 /// // _ => eprintln!("Logout behavior when not authenticated is unexpected."),
389 /// // }
390 /// # Ok(())
391 /// # }
392 /// ```
393 pub async fn logout(&mut self) -> Result<(), ParseError> {
394 if self.client.session_token.is_none() {
395 return Err(ParseError::SessionTokenMissing);
396 }
397 // Logout does not take a body and expects an empty JSON {} or specific success response
398 let result: Result<Value, _> = self
399 .client
400 ._request(Method::POST, "logout", None::<&Value>, false, None)
401 .await;
402 match result {
403 Ok(_value) => {
404 // Parse server returns an empty JSON object {} on successful logout
405 self.client.session_token = None;
406 Ok(())
407 }
408 Err(e) => Err(e),
409 }
410 }
411
412 // POST /requestPasswordReset - public, no session token needed
413 /// Requests a password reset email to be sent to the user associated with the given email address.
414 ///
415 /// This method sends a POST request to the `/requestPasswordReset` endpoint with the user's email.
416 /// The Parse Server then handles sending the password reset email if a user with that email exists.
417 /// This operation does not require a session token and can be called publicly.
418 ///
419 /// # Type Parameters
420 ///
421 /// * `T`: The type of the `email_data` argument. This type must implement `Serialize`, `Send`, and `Sync`.
422 /// Commonly, this will be [`PasswordResetRequest`](crate::user::PasswordResetRequest).
423 ///
424 /// # Arguments
425 ///
426 /// * `email_data`: A reference to the data containing the email address for the password reset request.
427 ///
428 /// # Returns
429 ///
430 /// A `Result` containing `()` (an empty tuple) if the request is successfully sent to the server,
431 /// or a `ParseError` if the request fails (e.g., invalid email format, network issue).
432 /// Note: A successful response means the request was accepted by the server, not necessarily that
433 /// a user with that email exists or that an email was actually sent (to prevent leaking user information).
434 ///
435 /// # Examples
436 ///
437 /// ```rust,no_run
438 /// use parse_rs::{Parse, ParseError, user::PasswordResetRequest};
439 ///
440 /// # #[tokio::main]
441 /// # async fn main() -> Result<(), ParseError> {
442 /// # let server_url = std::env::var("PARSE_SERVER_URL").unwrap_or_else(|_| "http://localhost:1338/parse".to_string());
443 /// # let app_id = std::env::var("PARSE_APP_ID").unwrap_or_else(|_| "myAppId".to_string());
444 /// # let mut client = Parse::new(&server_url, &app_id, None, None, None)?;
445 ///
446 /// let email_for_reset = "user_to_reset@example.com";
447 /// let reset_request_data = PasswordResetRequest { email: email_for_reset };
448 ///
449 /// match client.user().request_password_reset(&reset_request_data).await {
450 /// Ok(_) => println!("Password reset request sent for email: {}", email_for_reset),
451 /// Err(e) => eprintln!("Failed to send password reset request for email '{}': {}", email_for_reset, e),
452 /// }
453 /// # Ok(())
454 /// # }
455 /// ```
456 pub async fn request_password_reset<T: Serialize + Send + Sync>(
457 &self,
458 email_data: &T, // Changed from PasswordResetRequest to generic T
459 ) -> Result<(), ParseError> {
460 // request_password_reset expects an empty JSON {} or specific success response
461 let result: Result<Value, _> = self
462 .client
463 ._request(
464 Method::POST,
465 "requestPasswordReset",
466 Some(email_data),
467 false,
468 None,
469 )
470 .await;
471 match result {
472 Ok(_value) => Ok(()), // Expects empty {} on success
473 Err(e) => Err(e),
474 }
475 }
476
477 // GET /users/me - but with a different session token to become that user
478 // This is a tricky one. The `become` operation itself is a GET to /users/me, but authenticated with the *target* session token.
479 // The client's current session token is replaced upon success.
480 /// Allows the current client to "become" another user by using that user's session token.
481 ///
482 /// This method makes a GET request to `/users/me`, but authenticates it using the
483 /// `session_token_to_become` provided as an argument, instead of the client's current session token.
484 /// If the provided session token is valid, the server responds with the details of the user
485 /// associated with that token. Crucially, upon a successful response, this method **replaces**
486 /// the `Parse`'s current `session_token` with `session_token_to_become`.
487 ///
488 /// This is a powerful operation and should be used with caution, typically in administrative
489 /// contexts or when implementing features like "Log in as user" for support purposes.
490 /// The client performing this operation must have access to the target user's session token.
491 ///
492 /// # Arguments
493 ///
494 /// * `session_token_to_become`: A string slice representing the session token of the user
495 /// to become.
496 ///
497 /// # Returns
498 ///
499 /// A `Result` containing the [`ParseUser`](crate::user::ParseUser) object for the user
500 /// whose session token was provided, if the operation is successful. Returns a `ParseError`
501 /// if the provided session token is invalid, expired, or if any other error occurs during
502 /// the request.
503 ///
504 /// # Examples
505 ///
506 /// ```rust,no_run
507 /// use parse_rs::{Parse, ParseError};
508 ///
509 /// # #[tokio::main]
510 /// # async fn main() -> Result<(), ParseError> {
511 /// # let server_url = std::env::var("PARSE_SERVER_URL").unwrap_or_else(|_| "http://localhost:1338/parse".to_string());
512 /// # let app_id = std::env::var("PARSE_APP_ID").unwrap_or_else(|_| "myAppId".to_string());
513 /// # let mut client = Parse::new(&server_url, &app_id, None, None, None)?;
514 ///
515 /// // Assume `target_user_session_token` is a valid session token for another user, obtained securely.
516 /// let target_user_session_token = "r:someValidSessionTokenForAnotherUser";
517 ///
518 /// println!("Client's current session token before 'become': {:?}", client.session_token());
519 ///
520 /// match client.user().become_user(target_user_session_token).await {
521 /// Ok(newly_become_user) => {
522 /// println!("Successfully became user '{}' (ID: {}).",
523 /// newly_become_user.username,
524 /// newly_become_user.object_id.as_deref().unwrap_or("N/A"));
525 /// println!("Client's session token is now: {:?}", client.session_token());
526 /// assert_eq!(client.session_token(), Some(target_user_session_token));
527 /// }
528 /// Err(e) => {
529 /// eprintln!("Failed to become user with token '{}': {}", target_user_session_token, e);
530 /// // Client's original session token should be restored if 'become' failed.
531 /// println!("Client's session token after failed 'become': {:?}", client.session_token());
532 /// }
533 /// }
534 /// # Ok(())
535 /// # }
536 /// ```
537 pub async fn become_user(
538 &mut self,
539 session_token_to_become: &str,
540 ) -> Result<ParseUser, ParseError> {
541 // Temporarily override client's auth for this specific request
542 // This requires a way to make _request use a specific session token, or a dedicated _request_with_token method.
543 // Current _request uses self.session_token or master_key.
544 // A simple way is to clone the client, set the token, make the request, then update original client.
545 // Or, modify _request to accept an optional override token.
546
547 // For now, let's assume _request needs modification or we use a more direct approach for this one-off auth.
548 // The most straightforward modification to _request would be to allow an optional override token.
549 // Let's simulate this by creating a temporary request builder here, which is not ideal as it bypasses _request's error handling.
550
551 // Ideal approach: Modify _request to handle an override token.
552 // Fallback: Direct reqwest call for this specific case.
553 // Given current _request, this is hard to do cleanly without modifying _request signature or logic.
554
555 // Let's assume for now that we will add a specialized method or enhance _request later.
556 // For this refactor, we'll placeholder it or acknowledge it needs a more specific implementation.
557 // One way to achieve this with current _request: temporarily set self.session_token, then revert.
558 let original_token = self.client.session_token.clone();
559 self.client.session_token = Some(session_token_to_become.to_string());
560
561 match self
562 .client
563 ._request(Method::GET, "users/me", None::<&Value>, false, None)
564 .await
565 {
566 Ok(user_data) => {
567 // self.session_token is already set to session_token_to_become, so this is correct.
568 Ok(user_data)
569 }
570 Err(e) => {
571 self.client.session_token = original_token; // Revert on error
572 Err(e)
573 } // If successful, self.session_token remains as session_token_to_become
574 }
575 }
576}