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