pocketbase_rs/records/auth/
auth_with_password.rs

1use serde::Serialize;
2use serde_json::Value;
3use thiserror::Error;
4
5use crate::{AuthStore, Collection, ErrorResponse};
6
7#[derive(Clone, Default, Serialize)]
8struct Credentials<'a> {
9    pub(crate) identity: &'a str,
10    pub(crate) password: &'a str,
11}
12
13/// Represents errors that can occur during the authentication process with the `PocketBase` API.
14///
15/// This enum defines various error types that may arise when attempting to authenticate,
16/// each providing details about the specific issue encountered.
17#[derive(Error, Debug)]
18pub enum AuthenticationError {
19    /// Communication with the `PocketBase` API was successful,
20    /// but returned a [400 Bad Request]("https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/400") HTTP error response.
21    ///
22    /// Tip: The credentials you provided may be incorrect.
23    #[error("Authentication failed: Invalid Credentials. Given email and/or password is wrong.")]
24    InvalidCredentials,
25    /// Email and/or Password cannot be empty.
26    ///
27    /// This variant indicates that certain fields in the authentication request need to be validated.
28    /// The fields are represented as booleans:
29    ///
30    /// - `identity`: is blank and shouldn't be.
31    /// - `password`: is blank and shouldn't be.
32    #[error("Authentication failed: Empty Credential Field. Given email and/or password is empty.")]
33    EmptyField {
34        /// Is identity blank.
35        identity: bool,
36        /// Is password blank.
37        password: bool,
38    },
39    /// The provided identity must be an email address.
40    ///
41    /// This variant indicates that the authentication request failed because the provided identity
42    /// does not conform to the expected email format. The `PocketBase` API requires the identity to
43    /// be a valid email address for authentication.
44    #[error("Authentication failed. Given identity is not a valid email.")]
45    IdentityMustBeEmail,
46    /// An HTTP error occurred while communicating with the `PocketBase` API.
47    ///
48    /// This variant wraps a [`reqwest::Error`] and indicates that the request could not be completed
49    /// due to network issues, invalid URL, timeouts, etc.
50    #[error("Authentication failed. Couldn't reach the PocketBase API: {0}")]
51    HttpError(reqwest::Error),
52    /// When something unexpected was returned by the `PocketBase` REST API.
53    ///
54    /// Would usually mean that there is an error somewhere in this API wrapper.
55    #[error(
56        "Authentication failed due to an unexpected response. Usually means a problem in the PocketBase API's wrapper."
57    )]
58    UnexpectedResponse,
59    /// Occurs when you try to authenticate a `PocketBase` client without providing the collection name.
60    #[error(
61        "Authentication failed due to missing collection name. [Example: PocketBaseClientBuilder::new(\"\")"
62    )]
63    MissingCollection,
64}
65
66impl From<reqwest::Error> for AuthenticationError {
67    fn from(error: reqwest::Error) -> Self {
68        Self::HttpError(error)
69    }
70}
71
72impl Collection<'_> {
73    /// Authenticate with combination of **email**/**username** and **password**.
74    ///
75    /// On success, the auth token is automatically stored and used for subsequent requests.
76    ///
77    /// # Example
78    /// ```rust,ignore
79    /// let auth_data = pb.collection("users")
80    ///     .auth_with_password("YOUR_EMAIL_OR_USERNAME", "YOUR_PASSWORD")
81    ///     .await?;
82    ///
83    /// println!("Token: {}", auth_data.token);
84    /// ```
85    pub async fn auth_with_password(
86        &mut self,
87        identity: &str,
88        password: &str,
89    ) -> Result<AuthStore, AuthenticationError> {
90        let uri = format!(
91            "{}/api/collections/{}/auth-with-password",
92            self.client.base_url, self.name
93        );
94
95        let credentials = Credentials { identity, password };
96
97        let response = self
98            .client
99            .request_post_json(&uri, &credentials)
100            .send()
101            .await?;
102
103        if response.status().is_success() {
104            let auth_store = response.json::<AuthStore>().await?;
105
106            self.client.update_auth_store(auth_store.clone());
107
108            return Ok(auth_store);
109        }
110
111        if response.status() == reqwest::StatusCode::BAD_REQUEST {
112            let error_response: ErrorResponse =
113                response.json().await.unwrap_or_else(|_| ErrorResponse {
114                    code: 400,
115                    message: "Unknown error".to_string(),
116                    data: None,
117                });
118
119            if let Some(ref data) = error_response.data {
120                // {
121                //     "code": 400,
122                //     "message": "Failed to authenticate.",
123                //     "data": {}
124                // }
125                if data.as_object().is_some_and(serde_json::Map::is_empty) {
126                    return Err(AuthenticationError::InvalidCredentials);
127                }
128
129                // Check for specific field validation errors
130                let identity_error = data
131                    .get("identity")
132                    .and_then(|v| v.get("code").and_then(Value::as_str));
133
134                match identity_error {
135                    // {
136                    //     "code": 400,
137                    //     "message": "Something went wrong while processing your request.",
138                    //     "data": {
139                    //       "identity": {
140                    //         "code": "validation_is_email",
141                    //         "message": "Must be a valid email address."
142                    //       }
143                    //     }
144                    // }
145                    Some("validation_is_email") => {
146                        return Err(AuthenticationError::IdentityMustBeEmail);
147                    }
148
149                    // {
150                    //     "code": 400,
151                    //     "message": "Something went wrong while processing your request.",
152                    //     "data": {
153                    //       "identity": {
154                    //         "code": "validation_required",
155                    //         "message": "Cannot be blank."
156                    //       },
157                    //       "password": {
158                    //         "code": "validation_required",
159                    //         "message": "Cannot be blank."
160                    //       }
161                    //     }
162                    // }
163                    Some("validation_required") => {
164                        return Err(AuthenticationError::EmptyField {
165                            identity: identity_error.is_some(),
166                            password: data.get("password").is_some(),
167                        });
168                    }
169                    None => {
170                        let password_error = data.get("password").is_some();
171                        return Err(AuthenticationError::EmptyField {
172                            identity: false,
173                            password: password_error,
174                        });
175                    }
176                    _ => {}
177                }
178            }
179
180            // {
181            //     "code": 400,
182            //     "message": "Failed to authenticate.",
183            //     "data": {}
184            // }
185            return Err(AuthenticationError::InvalidCredentials);
186        }
187
188        Err(AuthenticationError::UnexpectedResponse)
189    }
190}