A wrapper around reqwest to make working with Pocketbase a breeze

Add more authentication methods and documentation

dekker.one 8b206504 8993706a

verified
+506 -18
+217 -5
src/auth.rs
··· 1 + use std::{ 2 + sync::{Arc, RwLock}, 3 + time::Duration, 4 + }; 5 + 1 6 use base64::{Engine, prelude::BASE64_STANDARD_NO_PAD}; 2 7 use reqwest::{Method, header::HeaderMap}; 3 - use serde::Deserialize; 8 + use serde::{Deserialize, Deserializer, Serialize, Serializer}; 4 9 use serde_json::{Value, json}; 5 10 6 - use crate::{Error, JsonObject, record::RecordService}; 11 + use crate::{Error, JsonObject, PocketBase, PocketBaseInner, record::RecordService}; 7 12 8 13 #[derive(Debug, Clone)] 9 14 pub struct AuthStore { 10 - pub(crate) token: String, 11 - record: RecordModel, 15 + pub token: String, 16 + pub record: RecordModel, 17 + } 18 + 19 + #[derive(Clone, Debug, Deserialize, Serialize)] 20 + pub struct AuthMethodList { 21 + pub mfa: AuthMethodMfa, 22 + pub oauth2: AuthMethodOAuth2, 23 + pub otp: AuthMethodOtp, 24 + pub password: AuthMethodPassword, 25 + } 26 + 27 + #[derive(Clone, Debug, Deserialize, Serialize)] 28 + pub struct AuthMethodMfa { 29 + #[serde( 30 + deserialize_with = "deserialize_duration", 31 + serialize_with = "serialize_duration" 32 + )] 33 + pub duration: Duration, 34 + pub enabled: bool, 35 + } 36 + 37 + #[derive(Clone, Debug, Deserialize, Serialize)] 38 + pub struct AuthMethodOAuth2 { 39 + pub enabled: bool, 40 + pub providers: Vec<AuthMethodProvider>, 41 + } 42 + 43 + #[derive(Clone, Debug, Deserialize, Serialize)] 44 + pub struct AuthMethodOtp { 45 + #[serde( 46 + deserialize_with = "deserialize_duration", 47 + serialize_with = "serialize_duration" 48 + )] 49 + pub duration: Duration, 50 + pub enabled: bool, 51 + } 52 + 53 + #[derive(Clone, Debug, Deserialize, Serialize)] 54 + #[serde(rename_all = "camelCase")] 55 + pub struct AuthMethodProvider { 56 + pub auth_url: String, 57 + pub code_challenge: String, 58 + pub code_challenge_method: String, 59 + pub code_verifier: String, 60 + pub display_name: String, 61 + pub name: String, 62 + pub pkce: Option<bool>, 63 + pub state: String, 64 + } 65 + 66 + #[derive(Clone, Debug, Deserialize, Serialize)] 67 + #[serde(rename_all = "camelCase")] 68 + pub struct AuthMethodPassword { 69 + pub enabled: bool, 70 + pub identity_fields: Vec<String>, 12 71 } 13 72 14 73 #[derive(Debug, Clone)] ··· 19 78 } 20 79 21 80 #[derive(Debug, Clone)] 81 + pub struct AuthWithOtpBuilder<'a> { 82 + pub(crate) record_service: RecordService<'a>, 83 + pub(crate) otp_id: &'a str, 84 + pub(crate) password: &'a str, 85 + pub(crate) expand_fields: Option<&'a str>, 86 + pub(crate) filter_fields: Option<&'a str>, 87 + } 88 + 89 + #[derive(Debug, Clone)] 22 90 pub struct AuthWithPasswordBuilder<'a> { 23 91 pub(crate) record_service: RecordService<'a>, 24 92 pub(crate) identity: &'a str, ··· 27 95 pub(crate) filter_fields: Option<&'a str>, 28 96 } 29 97 98 + #[derive(Debug, Clone)] 99 + pub struct ImpersonateBuilder<'a> { 100 + pub(crate) record_service: RecordService<'a>, 101 + pub(crate) id: &'a str, 102 + pub(crate) duration: Option<Duration>, 103 + pub(crate) expand_fields: Option<&'a str>, 104 + pub(crate) filter_fields: Option<&'a str>, 105 + } 106 + 30 107 #[derive(Debug, Deserialize, Clone)] 31 108 pub struct RecordAuth { 32 109 token: String, ··· 36 113 #[derive(Debug, Deserialize, Clone)] 37 114 #[serde(transparent)] 38 115 pub struct RecordModel { 39 - data: JsonObject, 116 + pub(crate) data: JsonObject, 117 + } 118 + 119 + fn deserialize_duration<'de, D>(deserializer: D) -> Result<Duration, D::Error> 120 + where 121 + D: Deserializer<'de>, 122 + { 123 + u64::deserialize(deserializer).map(Duration::from_secs) 40 124 } 41 125 42 126 #[cfg(target_arch = "wasm32")] ··· 57 141 .duration_since(UNIX_EPOCH) 58 142 .unwrap() 59 143 .as_secs() 144 + } 145 + 146 + fn serialize_duration<S>(dur: &Duration, serializer: S) -> Result<S::Ok, S::Error> 147 + where 148 + S: Serializer, 149 + { 150 + dur.as_secs().serialize(serializer) 60 151 } 61 152 62 153 impl AuthStore { ··· 133 224 } 134 225 } 135 226 227 + impl<'a> AuthWithOtpBuilder<'a> { 228 + pub fn expand_fields(mut self, field_expression: &'a str) -> Self { 229 + self.expand_fields = Some(field_expression); 230 + self 231 + } 232 + 233 + pub fn filter_fields(mut self, field_expression: &'a str) -> Self { 234 + self.filter_fields = Some(field_expression); 235 + self 236 + } 237 + 238 + pub async fn send(self) -> Result<RecordAuth, Error> { 239 + let body = json!({"otpId": self.otp_id, "password": self.password}); 240 + let mut params = Vec::new(); 241 + if let Some(expand) = self.expand_fields { 242 + params.push(("expand", expand)); 243 + } 244 + if let Some(fields) = self.filter_fields { 245 + params.push(("fields", fields)); 246 + } 247 + 248 + let auth: RecordAuth = self 249 + .record_service 250 + .client 251 + .send( 252 + &format!( 253 + "/api/collections/{}/auth-with-otp", 254 + self.record_service.collection 255 + ), 256 + Method::POST, 257 + HeaderMap::new(), 258 + &params, 259 + Some(&body), 260 + ) 261 + .await?; 262 + self.record_service.client.with_auth_store(AuthStore { 263 + token: auth.token.clone(), 264 + record: auth.record.clone(), 265 + }); 266 + debug_assert!({ 267 + let read = self.record_service.client.inner.read().unwrap(); 268 + read.auth_store.as_ref().unwrap().is_valid() 269 + }); 270 + Ok(auth) 271 + } 272 + } 273 + 136 274 impl<'a> AuthWithPasswordBuilder<'a> { 137 275 pub fn expand_fields(mut self, field_expression: &'a str) -> Self { 138 276 self.expand_fields = Some(field_expression); ··· 177 315 read.auth_store.as_ref().unwrap().is_valid() 178 316 }); 179 317 Ok(auth) 318 + } 319 + } 320 + 321 + impl<'a> ImpersonateBuilder<'a> { 322 + /// Set the duration that the generated auth token will be valid. 323 + /// 324 + /// If duration is not set, then the generated auth token will fallback to the 325 + /// default collection auth token duration. 326 + pub fn duration(mut self, duration: Duration) -> Self { 327 + self.duration = Some(duration); 328 + self 329 + } 330 + 331 + pub fn expand_fields(mut self, field_expression: &'a str) -> Self { 332 + self.expand_fields = Some(field_expression); 333 + self 334 + } 335 + 336 + pub fn filter_fields(mut self, field_expression: &'a str) -> Self { 337 + self.filter_fields = Some(field_expression); 338 + self 339 + } 340 + 341 + pub async fn send(self) -> Result<(PocketBase, RecordAuth), Error> { 342 + let mut params = Vec::new(); 343 + if let Some(expand) = self.expand_fields { 344 + params.push(("expand", expand)); 345 + } 346 + if let Some(fields) = self.filter_fields { 347 + params.push(("fields", fields)); 348 + } 349 + 350 + let auth: RecordAuth = self 351 + .record_service 352 + .client 353 + .send( 354 + &format!( 355 + "/api/collections/{}/impersonate/{}", 356 + self.record_service.collection, self.id 357 + ), 358 + Method::POST, 359 + HeaderMap::new(), 360 + &params, 361 + if let Some(dur) = self.duration { 362 + Some(json!({ "duration": dur.as_secs() })) 363 + } else { 364 + None 365 + } 366 + .as_ref(), 367 + ) 368 + .await?; 369 + 370 + let client = { 371 + let inner = self.record_service.client.inner.read().unwrap(); 372 + PocketBase { 373 + inner: Arc::new(RwLock::new(PocketBaseInner { 374 + reqwest: inner.reqwest.clone(), 375 + auth_store: None, 376 + base_url: inner.base_url.clone(), 377 + lang: inner.lang.clone(), 378 + })), 379 + } 380 + }; 381 + 382 + client.with_auth_store(AuthStore { 383 + token: auth.token.clone(), 384 + record: auth.record.clone(), 385 + }); 386 + debug_assert!({ 387 + let read = client.inner.read().unwrap(); 388 + read.auth_store.as_ref().unwrap().is_valid() 389 + }); 390 + 391 + Ok((client, auth)) 180 392 } 181 393 } 182 394
+6
src/health.rs
··· 5 5 use crate::{Error, PocketBase}; 6 6 7 7 #[derive(Clone, Debug)] 8 + /// Service that handles the Health APIs. 8 9 pub struct HealthService<'a> { 10 + /// Reference to the PocketBase client. 9 11 pub(super) client: &'a PocketBase, 12 + /// Optional comma-separated list of fields to include in the response. 10 13 pub(super) fields: Option<String>, 11 14 } 12 15 13 16 #[derive(Clone, Debug, Deserialize)] 14 17 pub struct HealthCheck { 18 + /// Status message. 15 19 pub message: String, 20 + /// Additional data. 16 21 pub data: Map<String, Value>, 17 22 } 18 23 19 24 impl HealthService<'_> { 25 + /// Checks the health status of the api. 20 26 pub async fn check(self) -> Result<HealthCheck, Error> { 21 27 let mut params = Vec::new(); 22 28 if let Some(fields) = self.fields {
+31 -5
src/lib.rs
··· 7 7 use serde::{Serialize, de::DeserializeOwned}; 8 8 use serde_json::{Map, Value}; 9 9 use thiserror::Error; 10 - use url::{ParseError, Url}; 10 + use url::Url; 11 11 12 12 pub mod auth; 13 13 pub mod health; ··· 15 15 16 16 use crate::{auth::AuthStore, health::HealthService, record::RecordService}; 17 17 18 - pub type Error = reqwest::Error; 18 + /// Type alias for reqwest's error type, that should be generally used if an 19 + /// error is returned. 20 + type Error = reqwest::Error; 19 21 20 - pub type JsonObject = Map<String, Value>; 22 + /// Type alias for a type that can represent general JSON objects. 23 + type JsonObject = Map<String, Value>; 21 24 22 25 #[derive(Clone, Debug)] 26 + /// Main PocketBase API client. 23 27 pub struct PocketBase { 24 28 inner: Arc<RwLock<PocketBaseInner>>, 25 29 } 26 30 27 31 #[derive(Clone, Debug)] 32 + /// Inner storage for the PocketBase API client. 28 33 struct PocketBaseInner { 29 - reqwest: Client, 34 + /// Storage of the authenticated user and the authenticated token, if any. 30 35 auth_store: Option<AuthStore>, 36 + /// The PocketBase backend base url address (e.g. `http://127.0.0.1:8090`). 31 37 base_url: Url, 38 + /// Language code that will be sent with the requests to the server as 39 + /// `Accept-Language` header. 40 + /// 41 + /// Will be set to `en-US` by default. 32 42 lang: String, 43 + /// Reqwest [`Client`] used. 44 + reqwest: Client, 33 45 } 34 46 35 47 impl PocketBase { 48 + /// Clears the previously stored token and record auth data. 36 49 pub fn auth_clear<'a>(&'a self) { 37 50 let mut write = self.inner.write().unwrap(); 38 51 write.auth_store = None; 39 52 } 40 53 54 + /// Returns the [`RecordService`] associated to the specified collection. 41 55 pub fn collection<'a>(&'a self, collection: &'a str) -> RecordService<'a> { 42 56 RecordService { 43 57 client: self, ··· 45 59 } 46 60 } 47 61 62 + /// Return an instance of the service that handles the Health APIs. 48 63 pub fn health(&self) -> HealthService { 49 64 HealthService { 50 65 client: self, ··· 52 67 } 53 68 } 54 69 70 + /// Get the language code that will be sent with the requests to the server as 71 + /// `Accept-Language` header. 55 72 pub fn lang(&self) -> String { 56 73 self.inner.read().unwrap().lang.clone() 57 74 } 58 75 59 - pub fn new(base_url: &str) -> Result<Self, ParseError> { 76 + /// Create a new [`PocketBase`] instance using the provided base URL for the 77 + /// PocketBase instance (e.g. `http://127.0.0.1:8090`). 78 + pub fn new(base_url: &str) -> Result<Self, url::ParseError> { 60 79 let base_url = Url::parse(base_url)?; 61 80 let inner = PocketBaseInner { 62 81 reqwest: Client::new(), ··· 69 88 }) 70 89 } 71 90 91 + /// Internal method used to send requests to the Pockatbase instance. 72 92 async fn send<Q, B, T>( 73 93 &self, 74 94 path: &str, ··· 125 145 response.error_for_status()?.json().await 126 146 } 127 147 148 + /// Internal method to set the authentication store when using one of the 149 + /// authentication methods. 128 150 fn with_auth_store(&self, auth: AuthStore) { 129 151 let mut write = self.inner.write().unwrap(); 130 152 write.auth_store = Some(auth); 131 153 } 132 154 155 + /// Set the language code that will be sent with the requests to the server as 156 + /// `Accept-Language` header. 157 + /// 158 + /// Will be`en-US`, if not set. 133 159 pub fn with_lang(self, lang: String) -> Self { 134 160 { 135 161 let mut write = self.inner.write().unwrap();
+252 -8
src/record.rs
··· 1 + use base64::{Engine, prelude::BASE64_STANDARD_NO_PAD}; 1 2 use reqwest::{Method, header::HeaderMap}; 2 3 use serde::{Deserialize, Serialize, de::DeserializeOwned}; 3 - use serde_json::{Map, Value}; 4 + use serde_json::{Map, Value, json}; 4 5 5 6 use crate::{ 6 7 Error, JsonObject, PocketBase, 7 - auth::{AuthRefreshBuilder, AuthWithPasswordBuilder}, 8 + auth::{ 9 + AuthMethodList, AuthRefreshBuilder, AuthWithOtpBuilder, AuthWithPasswordBuilder, 10 + ImpersonateBuilder, 11 + }, 8 12 }; 9 13 10 - pub struct CreateRequestBuilder<'a, T: Serialize = JsonObject> { 14 + const NO_PARAMS: &[&str] = &[]; 15 + 16 + pub struct CreateRequestBuilder<'a, T: Serialize> { 11 17 record_service: RecordService<'a>, 12 18 body: T, 13 19 } ··· 65 71 } 66 72 67 73 #[derive(Clone, Debug)] 68 - pub struct UpdateRequestBuilder<'a, T: Serialize = JsonObject> { 74 + pub struct UpdateRequestBuilder<'a, T: Serialize> { 69 75 record_service: RecordService<'a>, 70 76 body: T, 71 77 expand_fields: Option<&'a str>, ··· 100 106 } 101 107 } 102 108 103 - impl CreateRequestBuilder<'_, JsonObject> { 109 + impl<T: Extend<(String, Value)> + Serialize> CreateRequestBuilder<'_, T> { 104 110 pub fn fields<Iter: IntoIterator<Item = (String, Value)>>(mut self, patch: Iter) -> Self { 105 111 self.body.extend(patch); 106 112 self ··· 123 129 None::<&()>, 124 130 ) 125 131 .await 132 + // TODO: Dart SDK also implements the following behaviour. 133 + // 134 + // If the current [`AuthStore.record`] matches with the deleted id, then on 135 + // success the client [`AuthStore`] will be also cleared. 126 136 } 127 137 } 128 138 ··· 248 258 } 249 259 250 260 impl<'a> RecordService<'a> { 261 + /// Refreshes the current authenticated auth record instance and returns a new 262 + /// token and record data. 263 + /// 264 + /// On success this method automatically updates the client's AuthStore. 251 265 pub fn auth_refresh(&self) -> AuthRefreshBuilder<'a> { 252 266 AuthRefreshBuilder { 253 267 record_service: self.clone(), ··· 256 270 } 257 271 } 258 272 273 + /// Authenticate an auth record via OTP. 274 + /// 275 + /// On success this method automatically updates the client's [`AuthStore`]. 276 + pub fn auth_with_otp(&'a self, otp_id: &'a str, password: &'a str) -> AuthWithOtpBuilder<'a> { 277 + AuthWithOtpBuilder { 278 + record_service: self.clone(), 279 + otp_id, 280 + password, 281 + expand_fields: None, 282 + filter_fields: None, 283 + } 284 + } 285 + 286 + /// Authenticate an auth record by its username/email and password and returns 287 + /// a new auth token and record data. 288 + /// 289 + /// On success this method automatically updates the client's [`AuthStore`]. 259 290 pub fn auth_with_password( 260 291 &self, 261 292 identity: &'a str, ··· 270 301 } 271 302 } 272 303 273 - pub fn create(&self) -> CreateRequestBuilder<'a> { 304 + /// Confirms auth record new email address. 305 + /// 306 + /// If the current [`AuthStore.record`] matches with the record from the 307 + /// token, then on success the client [`AuthStore`] will be also cleared. 308 + pub async fn confirm_email_change(&self, token: &str, password: &str) -> Result<(), Error> { 309 + let () = self 310 + .client 311 + .send( 312 + &format!("/api/collections/{}/confirm-email-change", self.collection), 313 + Method::POST, 314 + HeaderMap::new(), 315 + NO_PARAMS, 316 + Some(&json!({ "token": token, "password": password })), 317 + ) 318 + .await?; 319 + 320 + let parts: Vec<_> = token.split(".").collect(); 321 + if parts.len() != 3 { 322 + return Ok(()); 323 + } 324 + let Ok(payload_str) = String::from_utf8(BASE64_STANDARD_NO_PAD.decode(parts[1]).unwrap()) 325 + else { 326 + return Ok(()); 327 + }; 328 + let Ok(payload) = serde_json::from_str::<JsonObject>(&payload_str) else { 329 + return Ok(()); 330 + }; 331 + 332 + { 333 + let inner = self.client.inner.read().unwrap(); 334 + if let Some(auth) = inner.auth_store.as_ref() { 335 + if auth.record.data.get("id") == payload.get("id") 336 + && auth.record.data.get("collectionId") == payload.get("collectionId") 337 + { 338 + self.client.auth_clear(); 339 + } 340 + } 341 + } 342 + Ok(()) 343 + } 344 + 345 + /// Confirms auth record password reset request. 346 + pub async fn confirm_password_reset( 347 + &self, 348 + token: &str, 349 + password: &str, 350 + password_confirm: &str, 351 + ) -> Result<(), Error> { 352 + self.client 353 + .send( 354 + &format!( 355 + "/api/collections/{}/confirm-password-reset", 356 + self.collection 357 + ), 358 + Method::POST, 359 + HeaderMap::new(), 360 + NO_PARAMS, 361 + Some( 362 + &json!({ "token": token, "password": password, "passwordConfirm": password_confirm }), 363 + ), 364 + ) 365 + .await 366 + } 367 + 368 + /// Confirms auth record email verification request. 369 + /// 370 + /// On success this method automatically updates the client's [`AuthStore`]. 371 + pub async fn confirm_verification(&self, token: &str) -> Result<(), Error> { 372 + let () = self 373 + .client 374 + .send( 375 + &format!("/api/collections/{}/confirm-verification", self.collection), 376 + Method::POST, 377 + HeaderMap::new(), 378 + NO_PARAMS, 379 + Some(&json!({ "token": token })), 380 + ) 381 + .await?; 382 + 383 + let parts: Vec<_> = token.split(".").collect(); 384 + if parts.len() != 3 { 385 + return Ok(()); 386 + } 387 + let Ok(payload_str) = String::from_utf8(BASE64_STANDARD_NO_PAD.decode(parts[1]).unwrap()) 388 + else { 389 + return Ok(()); 390 + }; 391 + let Ok(payload) = serde_json::from_str::<JsonObject>(&payload_str) else { 392 + return Ok(()); 393 + }; 394 + 395 + { 396 + let mut inner = self.client.inner.write().unwrap(); 397 + if let Some(auth) = inner.auth_store.as_mut() { 398 + if !auth 399 + .record 400 + .data 401 + .get("verified") 402 + .map(|v| v.as_bool().unwrap_or(false)) 403 + .unwrap_or(false) 404 + && auth.record.data.get("id") == payload.get("id") 405 + && auth.record.data.get("collectionId") == payload.get("collectionId") 406 + { 407 + let _ = auth.record.data.insert("verified".to_string(), true.into()); 408 + } 409 + } 410 + } 411 + Ok(()) 412 + } 413 + 414 + /// Creates a new item. 415 + pub fn create(&self) -> CreateRequestBuilder<'a, JsonObject> { 274 416 CreateRequestBuilder { 275 417 record_service: self.clone(), 276 418 body: Map::new(), 277 419 } 278 420 } 279 421 422 + /// Deletes a single record model by its id. 280 423 pub fn delete(&self, id: &'a str) -> DeleteRequestBuilder<'a> { 281 424 DeleteRequestBuilder { 282 425 record_service: self.clone(), ··· 284 427 } 285 428 } 286 429 430 + /// Returns a list with all items batch fetched at once. 287 431 pub fn get_full_list(&self) -> FullListRequestBuilder<'a> { 288 432 FullListRequestBuilder { 289 433 record_service: self.clone(), ··· 296 440 } 297 441 } 298 442 443 + /// Returns paginated items list. 299 444 pub fn get_list(&self, page: usize, per_page: usize) -> ListRequestBuilder<'a> { 300 445 ListRequestBuilder { 301 446 record_service: self.clone(), ··· 309 454 } 310 455 } 311 456 457 + /// Returns single item by its id. 458 + /// 459 + /// Throws `404` [`Error`] in case an empty id is provided. 312 460 pub fn get_one(&'a self, id: &'a str) -> ViewRequestBuilder<'a> { 313 461 ViewRequestBuilder { 314 462 record_service: self.clone(), ··· 318 466 } 319 467 } 320 468 321 - pub fn update(&self, id: &'a str) -> UpdateRequestBuilder<'a> { 469 + /// Authenticates with the specified recordId and returns a new client with 470 + /// the received auth token in a memory store. 471 + /// 472 + /// This action currently requires superusers privileges. 473 + pub fn impersonate(&'a self, id: &'a str) -> ImpersonateBuilder<'a> { 474 + ImpersonateBuilder { 475 + record_service: self.clone(), 476 + id, 477 + duration: None, 478 + expand_fields: None, 479 + filter_fields: None, 480 + } 481 + } 482 + 483 + /// Returns all available application auth methods. 484 + pub async fn list_auth_methods(&'a self) -> Result<AuthMethodList, Error> { 485 + self.client 486 + .send( 487 + &format!("/api/collections/{}/auth-methods", self.collection), 488 + Method::GET, 489 + HeaderMap::new(), 490 + NO_PARAMS, 491 + None::<&()>, 492 + ) 493 + .await 494 + } 495 + 496 + /// Sends auth record email change request to the provided email. 497 + pub async fn request_email_change(&self, new_email: &str) -> Result<(), Error> { 498 + self.client 499 + .send( 500 + &format!("/api/collections/{}/request-email-change", self.collection), 501 + Method::POST, 502 + HeaderMap::new(), 503 + NO_PARAMS, 504 + Some(&json!({ "newEmail": new_email })), 505 + ) 506 + .await 507 + } 508 + 509 + /// Sends auth record OTP request to the provided email. 510 + pub async fn request_otp(&self, email: &str) -> Result<String, Error> { 511 + #[derive(Deserialize)] 512 + #[serde(rename_all = "camelCase")] 513 + struct OtpIdResponse { 514 + otp_id: String, 515 + } 516 + 517 + let response: OtpIdResponse = self 518 + .client 519 + .send( 520 + &format!("/api/collections/{}/request-otp", self.collection), 521 + Method::POST, 522 + HeaderMap::new(), 523 + NO_PARAMS, 524 + Some(&json!({ "email": email })), 525 + ) 526 + .await?; 527 + 528 + Ok(response.otp_id) 529 + } 530 + 531 + /// Sends auth record password reset request. 532 + pub async fn request_password_reset(&self, email: &str) -> Result<(), Error> { 533 + self.client 534 + .send( 535 + &format!( 536 + "/api/collections/{}/request-password-reset", 537 + self.collection 538 + ), 539 + Method::POST, 540 + HeaderMap::new(), 541 + NO_PARAMS, 542 + Some(&json!({ "email": email })), 543 + ) 544 + .await 545 + } 546 + 547 + /// Sends auth record verification email request. 548 + pub async fn request_verification(&self, email: &str) -> Result<String, Error> { 549 + self.client 550 + .send( 551 + &format!("/api/collections/{}/request-verification", self.collection), 552 + Method::POST, 553 + HeaderMap::new(), 554 + NO_PARAMS, 555 + Some(&json!({ "email": email })), 556 + ) 557 + .await 558 + } 559 + 560 + /// Updates a single record model by its id. 561 + pub fn update(&self, id: &'a str) -> UpdateRequestBuilder<'a, JsonObject> { 322 562 UpdateRequestBuilder { 323 563 record_service: self.clone(), 324 564 id, ··· 453 693 Some(&self.body), 454 694 ) 455 695 .await 696 + // TODO: The following behaviour is also implemented in the Dart SDK. 697 + // 698 + // If the current [`AuthStore.record`] matches with the updated id, then on 699 + // success the client [`AuthStore`] will be updated with the result model. 456 700 } 457 701 } 458 702 459 - impl UpdateRequestBuilder<'_, JsonObject> { 703 + impl<T: Extend<(String, Value)> + Serialize> UpdateRequestBuilder<'_, T> { 460 704 pub fn fields<Iter: IntoIterator<Item = (String, Value)>>(mut self, patch: Iter) -> Self { 461 705 self.body.extend(patch); 462 706 self